[DRE-commits] [ruby-mysql2] 01/05: Imported Upstream version 0.4.2

zeha at debian.org zeha at debian.org
Wed Jan 6 01:16:16 UTC 2016


This is an automated email from the git hooks/post-receive script.

zeha pushed a commit to branch master
in repository ruby-mysql2.

commit 736196fba38ea391c4a4425d3a1bbbd1ffef0d3a
Author: Christian Hofstaedtler <zeha at debian.org>
Date:   Sat Dec 19 19:47:22 2015 +0000

    Imported Upstream version 0.4.2
---
 CHANGELOG.md                        |   1 +
 LICENSE                             |  21 +
 README.md                           |  75 +++-
 examples/eventmachine.rb            |   2 +-
 examples/threaded.rb                |  10 +-
 ext/mysql2/client.c                 | 345 ++++++++---------
 ext/mysql2/client.h                 |  22 +-
 ext/mysql2/extconf.rb               | 130 +++++--
 ext/mysql2/infile.c                 |   4 +-
 ext/mysql2/mysql2_ext.c             |   1 +
 ext/mysql2/mysql2_ext.h             |  11 +-
 ext/mysql2/mysql_enc_name_to_ruby.h |   4 +-
 ext/mysql2/mysql_enc_to_ruby.h      |  47 +--
 ext/mysql2/result.c                 | 626 +++++++++++++++++++++++-------
 ext/mysql2/result.h                 |  18 +-
 ext/mysql2/statement.c              | 494 ++++++++++++++++++++++++
 ext/mysql2/statement.h              |  19 +
 lib/mysql2.rb                       |  56 ++-
 lib/mysql2/client.rb                |  90 +++--
 lib/mysql2/console.rb               |   2 +-
 lib/mysql2/em.rb                    |  11 +-
 lib/mysql2/error.rb                 |  45 +--
 lib/mysql2/field.rb                 |   3 +
 lib/mysql2/statement.rb             |  17 +
 lib/mysql2/version.rb               |   2 +-
 metadata.yml                        |  87 ++---
 spec/em/em_spec.rb                  |  42 +-
 spec/mysql2/client_spec.rb          | 744 +++++++++++++++++++-----------------
 spec/mysql2/error_spec.rb           |  73 ++--
 spec/mysql2/result_spec.rb          | 421 ++++++++++----------
 spec/mysql2/statement_spec.rb       | 684 +++++++++++++++++++++++++++++++++
 spec/spec_helper.rb                 |   7 +
 spec/ssl/ca-cert.pem                |  17 +
 spec/ssl/ca-key.pem                 |  27 ++
 spec/ssl/ca.cnf                     |  22 ++
 spec/ssl/cert.cnf                   |  22 ++
 spec/ssl/client-cert.pem            |  17 +
 spec/ssl/client-key.pem             |  27 ++
 spec/ssl/client-req.pem             |  15 +
 spec/ssl/gen_certs.sh               |  48 +++
 spec/ssl/pkcs8-client-key.pem       |  28 ++
 spec/ssl/pkcs8-server-key.pem       |  28 ++
 spec/ssl/server-cert.pem            |  17 +
 spec/ssl/server-key.pem             |  27 ++
 spec/ssl/server-req.pem             |  15 +
 support/mysql_enc_to_ruby.rb        |  15 +-
 support/ruby_enc_to_mysql.rb        |   2 +-
 47 files changed, 3295 insertions(+), 1146 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..9a5bd0f
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1 @@
+Changes are maintained under [Releases](https://github.com/brianmario/mysql2/releases)
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..3a995e6
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2014 Brian Lopez
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 6ed86dd..c15cbeb 100644
--- a/README.md
+++ b/README.md
@@ -9,12 +9,14 @@ This one is not.
 
 It also forces the use of UTF-8 [or binary] for the connection [and all strings in 1.9, unless Encoding.default_internal is set then it'll convert from UTF-8 to that encoding] and uses encoding-aware MySQL API calls where it can.
 
-The API consists of two classes:
+The API consists of three classes:
 
 `Mysql2::Client` - your connection to the database.
 
 `Mysql2::Result` - returned from issuing a #query on the connection. It includes Enumerable.
 
+`Mysql2::Statement` - returned from issuing a #prepare on the connection. Execute the statement to get a Result.
+
 ## Installing
 ### General Instructions
 ``` sh
@@ -56,6 +58,20 @@ This may be needed if you deploy to a system where these libraries
 are located somewhere different than on your build system.
 This overrides any rpath calculated by default or by the options above.
 
+* `--with-sanitize[=address,cfi,integer,memory,thread,undefined]` -
+Enable sanitizers for Clang / GCC. If no argument is given, try to enable
+all sanitizers or fail if none are available. If a command-separated list of
+specific sanitizers is given, configure will fail unless they all are available.
+Note that the some sanitizers may incur a performance penalty, and the Address
+Sanitizer may require a runtime library.
+To see line numbers in backtraces, declare these environment variables
+(adjust the llvm-symbolizer path as needed for your system):
+
+``` sh
+  export ASAN_SYMBOLIZER_PATH=/usr/bin/llvm-symbolizer-3.4
+  export ASAN_OPTIONS=symbolize=1
+```
+
 ### Linux and other Unixes
 
 You may need to install a package such as `libmysqlclient-dev` or `mysql-devel`;
@@ -153,6 +169,20 @@ results.each(:as => :array) do |row|
 end
 ```
 
+Prepared statements are supported, as well. In a prepared statement, use a `?`
+in place of each value and then execute the statement to retrieve a result set.
+Pass your arguments to the execute method in the same number and order as the
+question marks in the statement.
+
+``` ruby
+statement = @client.prepare("SELECT * FROM users WHERE login_count = ?")
+result1 = statement.execute(1)
+result2 = statement.execute(2)
+
+statement = @client.prepare("SELECT * FROM users WHERE last_login >= ? AND location LIKE ?")
+result = statement.execute(1, "CA")
+```
+
 ## Connection options
 
 You may set the following connection options in Mysql2::Client.new(...):
@@ -186,7 +216,8 @@ Setting any of the following options will enable an SSL connection, but only if
 your MySQL client library and server have been compiled with SSL support.
 MySQL client library defaults will be used for any parameters that are left out
 or set to nil. Relative paths are allowed, and may be required by managed
-hosting providers such as Heroku.
+hosting providers such as Heroku. Set `:sslverify => true` to require that the
+server presents a valid certificate.
 
 ``` ruby
 Mysql2::Client.new(
@@ -195,7 +226,8 @@ Mysql2::Client.new(
   :sslcert => '/path/to/client-cert.pem',
   :sslca => '/path/to/ca-cert.pem',
   :sslcapath => '/path/to/cacerts',
-  :sslcipher => 'DHE-RSA-AES256-SHA'
+  :sslcipher => 'DHE-RSA-AES256-SHA',
+  :sslverify => true,
   )
 ```
 
@@ -240,15 +272,26 @@ Yields:
 next_result: Unknown column 'A' in 'field list' (Mysql2::Error)
 ```
 
-See https://gist.github.com/1367987 for using MULTI_STATEMENTS with Active Record.
-
 ### Secure auth
 
 Starting wih MySQL 5.6.5, secure_auth is enabled by default on servers (it was disabled by default prior to this).
 When secure_auth is enabled, the server will refuse a connection if the account password is stored in old pre-MySQL 4.1 format.
 The MySQL 5.6.5 client library may also refuse to attempt a connection if provided an older format password.
-To bypass this restriction in the client, pass the option :secure_auth => false to Mysql2::Client.new().
-If using ActiveRecord, your database.yml might look something like this:
+To bypass this restriction in the client, pass the option `:secure_auth => false` to Mysql2::Client.new().
+
+### Flags option parsing
+
+The `:flags` parameter accepts an integer, a string, or an array. The integer
+form allows the client to assemble flags from constants defined under
+`Mysql2::Client` such as `Mysql2::Client::FOUND_ROWS`. Use a bitwise `|` (OR)
+to specify several flags.
+
+The string form will be split on whitespace and parsed as with the array form:
+Plain flags are added to the default flags, while flags prefixed with `-`
+(minus) are removed from the default flags.
+
+This allows easier use with ActiveRecord's database.yml, avoiding the need for magic flag numbers.
+For example, to disable protocol compression, and enable multiple statements and result sets:
 
 ``` yaml
 development:
@@ -259,13 +302,17 @@ development:
   password: my_password
   host: 127.0.0.1
   port: 3306
+  flags:
+    - -COMPRESS
+    - FOUND_ROWS
+    - MULTI_STATEMENTS
   secure_auth: false
 ```
 
 ### Reading a MySQL config file
 
 You may read configuration options from a MySQL configuration file by passing
-the `:default_file` and `:default_group` paramters. For example:
+the `:default_file` and `:default_group` parameters. For example:
 
 ``` ruby
 Mysql2::Client.new(:default_file => '/user/.my.cnf', :default_group => 'client')
@@ -273,7 +320,7 @@ Mysql2::Client.new(:default_file => '/user/.my.cnf', :default_group => 'client')
 
 ### Initial command on connect and reconnect
 
-If you specify the init_command option, the SQL string you provide will be executed after the connection is established.
+If you specify the `:init_command` option, the SQL string you provide will be executed after the connection is established.
 If `:reconnect` is set to `true`, init_command will also be executed after a successful reconnect.
 It is useful if you want to provide session options which survive reconnection.
 
@@ -437,13 +484,13 @@ As for field values themselves, I'm workin on it - but expect that soon.
 
 This gem is tested with the following Ruby versions on Linux and Mac OS X:
 
- * Ruby MRI 1.8.7, 1.9.2, 1.9.3, 2.0.0, 2.1.x, 2.2.x (ongoing patch releases)
+ * Ruby MRI 1.8.7, 1.9.3, 2.0.0, 2.1.x, 2.2.x
  * Ruby Enterprise Edition (based on MRI 1.8.7)
  * Rubinius 2.x
 
 This gem is tested with the following MySQL and MariaDB versions:
 
- * MySQL 5.0, 5.1, 5.5, 5.6, 5.7
+ * MySQL 5.5, 5.7
  * MySQL Connector/C 6.0 and 6.1 (primarily on Windows)
  * MariaDB 5.5, 10.0
 
@@ -536,4 +583,8 @@ though.
 * Yury Korolev (http://github.com/yury) - for TONS of help testing the Active Record adapter
 * Aaron Patterson (http://github.com/tenderlove) - tons of contributions, suggestions and general badassness
 * Mike Perham (http://github.com/mperham) - Async Active Record adapter (uses Fibers and EventMachine)
-* Aaron Stone (http://github.com/sodabrew) - additional client settings, local files, microsecond time, maintenance support.
+* Aaron Stone (http://github.com/sodabrew) - additional client settings, local files, microsecond time, maintenance support
+* Kouhei Ueno (https://github.com/nyaxt) - for the original work on Prepared Statements way back in 2012
+* John Cant (http://github.com/johncant) - polishing and updating Prepared Statements support
+* Justin Case (http://github.com/justincase) - polishing and updating Prepared Statements support and getting it merged
+* Tamir Duberstein (http://github.com/tamird) - for help with timeouts and all around updates and cleanups
diff --git a/examples/eventmachine.rb b/examples/eventmachine.rb
index a92ca4c..273f1bc 100644
--- a/examples/eventmachine.rb
+++ b/examples/eventmachine.rb
@@ -18,4 +18,4 @@ EM.run do
   defer2.callback do |result|
     puts "Result: #{result.to_a.inspect}"
   end
-end
\ No newline at end of file
+end
diff --git a/examples/threaded.rb b/examples/threaded.rb
index 7d3b961..489edaf 100644
--- a/examples/threaded.rb
+++ b/examples/threaded.rb
@@ -4,17 +4,15 @@ $LOAD_PATH.unshift 'lib'
 require 'mysql2'
 require 'timeout'
 
-threads = []
 # Should never exceed worst case 3.5 secs across all 20 threads
 Timeout.timeout(3.5) do
-  20.times do
-    threads << Thread.new do
+  20.times.map do
+    Thread.new do
       overhead = rand(3)
       puts ">> thread #{Thread.current.object_id} query, #{overhead} sec overhead"
       # 3 second overhead per query
       Mysql2::Client.new(:host => "localhost", :username => "root").query("SELECT sleep(#{overhead}) as result")
       puts "<< thread #{Thread.current.object_id} result, #{overhead} sec overhead"
     end
-  end
-  threads.each{|t| t.join }
-end
\ No newline at end of file
+  end.each(&:join)
+end
diff --git a/ext/mysql2/client.c b/ext/mysql2/client.c
index f5fdbbd..b2ac283 100644
--- a/ext/mysql2/client.c
+++ b/ext/mysql2/client.c
@@ -16,12 +16,12 @@
 
 VALUE cMysql2Client;
 extern VALUE mMysql2, cMysql2Error;
-static VALUE sym_id, sym_version, sym_async, sym_symbolize_keys, sym_as, sym_array, sym_stream;
-static ID intern_merge, intern_merge_bang, intern_error_number_eql, intern_sql_state_eql;
+static VALUE sym_id, sym_version, sym_header_version, sym_async, sym_symbolize_keys, sym_as, sym_array, sym_stream;
+static ID intern_brackets, intern_merge, intern_merge_bang, intern_new_with_args;
 
 #ifndef HAVE_RB_HASH_DUP
-static VALUE rb_hash_dup(VALUE other) {
-  return rb_funcall(rb_cHash, rb_intern("[]"), 1, other);
+VALUE rb_hash_dup(VALUE other) {
+  return rb_funcall(rb_cHash, intern_brackets, 1, other);
 }
 #endif
 
@@ -30,25 +30,12 @@ static VALUE rb_hash_dup(VALUE other) {
     rb_raise(cMysql2Error, "MySQL client is not initialized"); \
   }
 
-#define REQUIRE_CONNECTED(wrapper) \
-  REQUIRE_INITIALIZED(wrapper) \
-  if (!wrapper->connected && !wrapper->reconnect_enabled) { \
-    rb_raise(cMysql2Error, "closed MySQL connection"); \
-  }
-
 #define REQUIRE_NOT_CONNECTED(wrapper) \
   REQUIRE_INITIALIZED(wrapper) \
   if (wrapper->connected) { \
     rb_raise(cMysql2Error, "MySQL connection is already open"); \
   }
 
-#define MARK_CONN_INACTIVE(conn) \
-  wrapper->active_thread = Qnil;
-
-#define GET_CLIENT(self) \
-  mysql_client_wrapper *wrapper; \
-  Data_Get_Struct(self, mysql_client_wrapper, wrapper)
-
 /*
  * compatability with mysql-connector-c, where LIBMYSQL_VERSION is the correct
  * variable to use, but MYSQL_SERVER_VERSION gives the correct numbers when
@@ -136,16 +123,17 @@ static VALUE rb_raise_mysql2_error(mysql_client_wrapper *wrapper) {
   rb_enc_associate(rb_sql_state, rb_usascii_encoding());
 #endif
 
-  e = rb_funcall(cMysql2Error, rb_intern("new"), 2, rb_error_msg, LONG2FIX(wrapper->server_version));
-  rb_funcall(e, intern_error_number_eql, 1, UINT2NUM(mysql_errno(wrapper->client)));
-  rb_funcall(e, intern_sql_state_eql, 1, rb_sql_state);
+  e = rb_funcall(cMysql2Error, intern_new_with_args, 4,
+                 rb_error_msg,
+                 LONG2FIX(wrapper->server_version),
+                 UINT2NUM(mysql_errno(wrapper->client)),
+                 rb_sql_state);
   rb_exc_raise(e);
-  return Qnil;
 }
 
 static void *nogvl_init(void *ptr) {
   MYSQL *client;
-  mysql_client_wrapper *wrapper = (mysql_client_wrapper *)ptr;
+  mysql_client_wrapper *wrapper = ptr;
 
   /* may initialize embedded server and read /etc/services off disk */
   client = mysql_init(wrapper->client);
@@ -182,23 +170,31 @@ static void *nogvl_connect(void *ptr) {
  */
 static VALUE invalidate_fd(int clientfd)
 {
-#ifdef SOCK_CLOEXEC
+#ifdef O_CLOEXEC
   /* Atomically set CLOEXEC on the new FD in case another thread forks */
   int sockfd = open("/dev/null", O_RDWR | O_CLOEXEC);
-  if (sockfd < 0) {
-    /* Maybe SOCK_CLOEXEC is defined but not available on this kernel */
-    int sockfd = open("/dev/null", O_RDWR);
-    fcntl(sockfd, F_SETFD, FD_CLOEXEC);
-  }
 #else
-  /* Well we don't have SOCK_CLOEXEC, so just set FD_CLOEXEC quickly */
-  int sockfd = open("/dev/null", O_RDWR);
-  fcntl(sockfd, F_SETFD, FD_CLOEXEC);
+  /* Well we don't have O_CLOEXEC, trigger the fallback code below */
+  int sockfd = -1;
 #endif
 
   if (sockfd < 0) {
-    /*
-     * Cannot raise here, because one or both of the following may be true:
+    /* Either O_CLOEXEC wasn't defined at compile time, or it was defined at
+     * compile time, but isn't available at run-time. So we'll just be quick
+     * about setting FD_CLOEXEC now.
+     */
+    int flags;
+    sockfd = open("/dev/null", O_RDWR);
+    flags = fcntl(sockfd, F_GETFD);
+    /* Do the flags dance in case there are more defined flags in the future */
+    if (flags != -1) {
+      flags |= FD_CLOEXEC;
+      fcntl(sockfd, F_SETFD, flags);
+    }
+  }
+
+  if (sockfd < 0) {
+    /* Cannot raise here, because one or both of the following may be true:
      * a) we have no GVL (in C Ruby)
      * b) are running as a GC finalizer
      */
@@ -213,43 +209,47 @@ static VALUE invalidate_fd(int clientfd)
 #endif /* _WIN32 */
 
 static void *nogvl_close(void *ptr) {
-  mysql_client_wrapper *wrapper;
-  wrapper = ptr;
-  if (wrapper->connected) {
-    wrapper->active_thread = Qnil;
-    wrapper->connected = 0;
-#ifndef _WIN32
-    /* Invalidate the socket before calling mysql_close(). This prevents
-     * mysql_close() from sending a mysql-QUIT or from calling shutdown() on
-     * the socket. The difference is that invalidate_fd will drop this
-     * process's reference to the socket only, while a QUIT or shutdown()
-     * would render the underlying connection unusable, interrupting other
-     * processes which share this object across a fork().
-     */
-    if (invalidate_fd(wrapper->client->net.fd) == Qfalse) {
-      fprintf(stderr, "[WARN] mysql2 failed to invalidate FD safely, leaking some memory\n");
-      close(wrapper->client->net.fd);
-      return NULL;
-    }
-#endif
+  mysql_client_wrapper *wrapper = ptr;
 
-    mysql_close(wrapper->client); /* only used to free memory at this point */
+  if (wrapper->client) {
+    mysql_close(wrapper->client);
+    xfree(wrapper->client);
+    wrapper->client = NULL;
+    wrapper->connected = 0;
+    wrapper->active_thread = Qnil;
   }
 
   return NULL;
 }
 
+/* this is called during GC */
 static void rb_mysql_client_free(void *ptr) {
-  mysql_client_wrapper *wrapper = (mysql_client_wrapper *)ptr;
+  mysql_client_wrapper *wrapper = ptr;
   decr_mysql2_client(wrapper);
 }
 
 void decr_mysql2_client(mysql_client_wrapper *wrapper)
 {
   wrapper->refcount--;
+
   if (wrapper->refcount == 0) {
+#ifndef _WIN32
+    if (wrapper->connected) {
+      /* The client is being garbage collected while connected. Prevent
+       * mysql_close() from sending a mysql-QUIT or from calling shutdown() on
+       * the socket by invalidating it. invalidate_fd() will drop this
+       * process's reference to the socket only, while a QUIT or shutdown()
+       * would render the underlying connection unusable, interrupting other
+       * processes which share this object across a fork().
+       */
+      if (invalidate_fd(wrapper->client->net.fd) == Qfalse) {
+        fprintf(stderr, "[WARN] mysql2 failed to invalidate FD safely\n");
+        close(wrapper->client->net.fd);
+      }
+    }
+#endif
+
     nogvl_close(wrapper);
-    xfree(wrapper->client);
     xfree(wrapper);
   }
 }
@@ -259,7 +259,7 @@ static VALUE allocate(VALUE klass) {
   mysql_client_wrapper * wrapper;
   obj = Data_Make_Struct(klass, mysql_client_wrapper, rb_mysql_client_mark, rb_mysql_client_free, wrapper);
   wrapper->encoding = Qnil;
-  wrapper->active_thread = Qnil;
+  MARK_CONN_INACTIVE(self);
   wrapper->server_version = 0;
   wrapper->reconnect_enabled = 0;
   wrapper->connect_timeout = 0;
@@ -267,6 +267,7 @@ static VALUE allocate(VALUE klass) {
   wrapper->initialized = 0; /* means that that the wrapper is initialized */
   wrapper->refcount = 1;
   wrapper->client = (MYSQL*)xmalloc(sizeof(MYSQL));
+
   return obj;
 }
 
@@ -287,7 +288,7 @@ static VALUE rb_mysql_client_escape(RB_MYSQL_UNUSED VALUE klass, VALUE str) {
   oldLen = RSTRING_LEN(str);
   newStr = xmalloc(oldLen*2+1);
 
-  newLen = mysql_escape_string((char *)newStr, StringValuePtr(str), oldLen);
+  newLen = mysql_escape_string((char *)newStr, RSTRING_PTR(str), oldLen);
   if (newLen == oldLen) {
     /* no need to return a new ruby string if nothing changed */
     xfree(newStr);
@@ -332,18 +333,17 @@ static VALUE rb_mysql_info(VALUE self) {
 
 static VALUE rb_connect(VALUE self, VALUE user, VALUE pass, VALUE host, VALUE port, VALUE database, VALUE socket, VALUE flags) {
   struct nogvl_connect_args args;
-  time_t start_time, end_time;
-  unsigned int elapsed_time, connect_timeout;
+  time_t start_time, end_time, elapsed_time, connect_timeout;
   VALUE rv;
   GET_CLIENT(self);
 
-  args.host = NIL_P(host) ? NULL : StringValuePtr(host);
-  args.unix_socket = NIL_P(socket) ? NULL : StringValuePtr(socket);
-  args.port = NIL_P(port) ? 0 : NUM2INT(port);
-  args.user = NIL_P(user) ? NULL : StringValuePtr(user);
-  args.passwd = NIL_P(pass) ? NULL : StringValuePtr(pass);
-  args.db = NIL_P(database) ? NULL : StringValuePtr(database);
-  args.mysql = wrapper->client;
+  args.host        = NIL_P(host)     ? NULL : StringValueCStr(host);
+  args.unix_socket = NIL_P(socket)   ? NULL : StringValueCStr(socket);
+  args.port        = NIL_P(port)     ? 0    : NUM2INT(port);
+  args.user        = NIL_P(user)     ? NULL : StringValueCStr(user);
+  args.passwd      = NIL_P(pass)     ? NULL : StringValueCStr(pass);
+  args.db          = NIL_P(database) ? NULL : StringValueCStr(database);
+  args.mysql       = wrapper->client;
   args.client_flag = NUM2ULONG(flags);
 
   if (wrapper->connect_timeout)
@@ -360,7 +360,7 @@ static VALUE rb_connect(VALUE self, VALUE user, VALUE pass, VALUE host, VALUE po
         /* avoid an early timeout due to time truncating milliseconds off the start time */
         if (elapsed_time > 0)
           elapsed_time--;
-        if (elapsed_time >= wrapper->connect_timeout)
+        if (elapsed_time >= (time_t)wrapper->connect_timeout)
           break;
         connect_timeout = wrapper->connect_timeout - elapsed_time;
         mysql_options(wrapper->client, MYSQL_OPT_CONNECT_TIMEOUT, &connect_timeout);
@@ -381,10 +381,13 @@ static VALUE rb_connect(VALUE self, VALUE user, VALUE pass, VALUE host, VALUE po
 }
 
 /*
- * Immediately disconnect from the server, normally the garbage collector
- * will disconnect automatically when a connection is no longer needed.
- * Explicitly closing this will free up server resources sooner than waiting
- * for the garbage collector.
+ * Terminate the connection; call this when the connection is no longer needed.
+ * The garbage collector can close the connection, but doing so emits an
+ * "Aborted connection" error on the server and increments the Aborted_clients
+ * status variable.
+ *
+ * @see http://dev.mysql.com/doc/en/communication-errors.html
+ * @return [void]
  */
 static VALUE rb_mysql_client_close(VALUE self) {
   GET_CLIENT(self);
@@ -434,10 +437,9 @@ static void *nogvl_read_query_result(void *ptr) {
 }
 
 static void *nogvl_do_result(void *ptr, char use_result) {
-  mysql_client_wrapper *wrapper;
+  mysql_client_wrapper *wrapper = ptr;
   MYSQL_RES *result;
 
-  wrapper = (mysql_client_wrapper *)ptr;
   if (use_result) {
     result = mysql_use_result(wrapper->client);
   } else {
@@ -446,7 +448,7 @@ static void *nogvl_do_result(void *ptr, char use_result) {
 
   /* once our result is stored off, this connection is
      ready for another command to be issued */
-  wrapper->active_thread = Qnil;
+  MARK_CONN_INACTIVE(self);
 
   return result;
 }
@@ -499,9 +501,9 @@ static VALUE rb_mysql_client_async_result(VALUE self) {
   }
 
   current = rb_hash_dup(rb_iv_get(self, "@current_query_options"));
-  RB_GC_GUARD(current);
+  (void)RB_GC_GUARD(current);
   Check_Type(current, T_HASH);
-  resultObj = rb_mysql_result_to_obj(self, wrapper->encoding, current, result);
+  resultObj = rb_mysql_result_to_obj(self, wrapper->encoding, current, result, Qnil);
 
   return resultObj;
 }
@@ -515,7 +517,7 @@ struct async_query_args {
 static VALUE disconnect_and_raise(VALUE self, VALUE error) {
   GET_CLIENT(self);
 
-  wrapper->active_thread = Qnil;
+  MARK_CONN_INACTIVE(self);
   wrapper->connected = 0;
 
   /* Invalidate the MySQL socket to prevent further communication.
@@ -527,19 +529,16 @@ static VALUE disconnect_and_raise(VALUE self, VALUE error) {
   }
 
   rb_exc_raise(error);
-
-  return Qnil;
 }
 
 static VALUE do_query(void *args) {
-  struct async_query_args *async_args;
+  struct async_query_args *async_args = args;
   struct timeval tv;
-  struct timeval* tvp;
+  struct timeval *tvp;
   long int sec;
   int retval;
   VALUE read_timeout;
 
-  async_args = (struct async_query_args *)args;
   read_timeout = rb_iv_get(async_args->self, "@read_timeout");
 
   tvp = NULL;
@@ -577,11 +576,9 @@ static VALUE do_query(void *args) {
 }
 #else
 static VALUE finish_and_mark_inactive(void *args) {
-  VALUE self;
+  VALUE self = args;
   MYSQL_RES *result;
 
-  self = (VALUE)args;
-
   GET_CLIENT(self);
 
   if (!NIL_P(wrapper->active_thread)) {
@@ -591,13 +588,31 @@ static VALUE finish_and_mark_inactive(void *args) {
     result = (MYSQL_RES *)rb_thread_call_without_gvl(nogvl_store_result, wrapper, RUBY_UBF_IO, 0);
     mysql_free_result(result);
 
-    wrapper->active_thread = Qnil;
+    MARK_CONN_INACTIVE(self);
   }
 
   return Qnil;
 }
 #endif
 
+void rb_mysql_client_set_active_thread(VALUE self) {
+  VALUE thread_current = rb_thread_current();
+  GET_CLIENT(self);
+
+  // see if this connection is still waiting on a result from a previous query
+  if (NIL_P(wrapper->active_thread)) {
+    // mark this connection active
+    wrapper->active_thread = thread_current;
+  } else if (wrapper->active_thread == thread_current) {
+    rb_raise(cMysql2Error, "This connection is still waiting for a result, try again once you have the result");
+  } else {
+    VALUE inspect = rb_inspect(wrapper->active_thread);
+    const char *thr = StringValueCStr(inspect);
+
+    rb_raise(cMysql2Error, "This connection is in use by: %s", thr);
+  }
+}
+
 /* call-seq:
  *    client.abandon_results!
  *
@@ -632,74 +647,47 @@ static VALUE rb_mysql_client_abandon_results(VALUE self) {
  *    client.query(sql, options = {})
  *
  * Query the database with +sql+, with optional +options+.  For the possible
- * options, see @@default_query_options on the Mysql2::Client class.
+ * options, see default_query_options on the Mysql2::Client class.
  */
-static VALUE rb_mysql_client_query(int argc, VALUE * argv, VALUE self) {
+static VALUE rb_query(VALUE self, VALUE sql, VALUE current) {
 #ifndef _WIN32
   struct async_query_args async_args;
 #endif
   struct nogvl_send_query_args args;
-  int async = 0;
-  VALUE opts, current;
-  VALUE thread_current = rb_thread_current();
-#ifdef HAVE_RUBY_ENCODING_H
-  rb_encoding *conn_enc;
-#endif
   GET_CLIENT(self);
 
   REQUIRE_CONNECTED(wrapper);
   args.mysql = wrapper->client;
 
-  current = rb_hash_dup(rb_iv_get(self, "@query_options"));
-  RB_GC_GUARD(current);
+  (void)RB_GC_GUARD(current);
   Check_Type(current, T_HASH);
   rb_iv_set(self, "@current_query_options", current);
 
-  if (rb_scan_args(argc, argv, "11", &args.sql, &opts) == 2) {
-    rb_funcall(current, intern_merge_bang, 1, opts);
-
-    if (rb_hash_aref(current, sym_async) == Qtrue) {
-      async = 1;
-    }
-  }
-
-  Check_Type(args.sql, T_STRING);
+  Check_Type(sql, T_STRING);
 #ifdef HAVE_RUBY_ENCODING_H
-  conn_enc = rb_to_encoding(wrapper->encoding);
   /* 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(sql, rb_to_encoding(wrapper->encoding));
+#else
+  args.sql = sql;
 #endif
-  args.sql_ptr = StringValuePtr(args.sql);
+  args.sql_ptr = RSTRING_PTR(args.sql);
   args.sql_len = RSTRING_LEN(args.sql);
-
-  /* see if this connection is still waiting on a result from a previous query */
-  if (NIL_P(wrapper->active_thread)) {
-    /* mark this connection active */
-    wrapper->active_thread = thread_current;
-  } else if (wrapper->active_thread == thread_current) {
-    rb_raise(cMysql2Error, "This connection is still waiting for a result, try again once you have the result");
-  } else {
-    VALUE inspect = rb_inspect(wrapper->active_thread);
-    const char *thr = StringValueCStr(inspect);
-
-    rb_raise(cMysql2Error, "This connection is in use by: %s", thr);
-    RB_GC_GUARD(inspect);
-  }
-
   args.wrapper = wrapper;
 
+  rb_mysql_client_set_active_thread(self);
+
 #ifndef _WIN32
   rb_rescue2(do_send_query, (VALUE)&args, disconnect_and_raise, self, rb_eException, (VALUE)0);
 
-  if (!async) {
+  if (rb_hash_aref(current, sym_async) == Qtrue) {
+    return Qnil;
+  } else {
     async_args.fd = wrapper->client->net.fd;
     async_args.self = self;
 
     rb_rescue2(do_query, (VALUE)&async_args, disconnect_and_raise, self, rb_eException, (VALUE)0);
 
     return rb_mysql_client_async_result(self);
-  } else {
-    return Qnil;
   }
 #else
   do_send_query(&args);
@@ -736,9 +724,14 @@ static VALUE rb_mysql_client_real_escape(VALUE self, VALUE str) {
   oldLen = RSTRING_LEN(str);
   newStr = xmalloc(oldLen*2+1);
 
-  newLen = mysql_real_escape_string(wrapper->client, (char *)newStr, StringValuePtr(str), oldLen);
+  newLen = mysql_real_escape_string(wrapper->client, (char *)newStr, RSTRING_PTR(str), oldLen);
   if (newLen == oldLen) {
     /* no need to return a new ruby string if nothing changed */
+#ifdef HAVE_RUBY_ENCODING_H
+    if (default_internal_enc) {
+      str = rb_str_export_to_enc(str, default_internal_enc);
+    }
+#endif
     xfree(newStr);
     return str;
   } else {
@@ -800,17 +793,17 @@ static VALUE _mysql_client_options(VALUE self, int opt, VALUE value) {
       break;
 
     case MYSQL_READ_DEFAULT_FILE:
-      charval = (const char *)StringValuePtr(value);
+      charval = (const char *)StringValueCStr(value);
       retval  = charval;
       break;
 
     case MYSQL_READ_DEFAULT_GROUP:
-      charval = (const char *)StringValuePtr(value);
+      charval = (const char *)StringValueCStr(value);
       retval  = charval;
       break;
 
     case MYSQL_INIT_COMMAND:
-      charval = (const char *)StringValuePtr(value);
+      charval = (const char *)StringValueCStr(value);
       retval  = charval;
       break;
 
@@ -843,30 +836,23 @@ static VALUE _mysql_client_options(VALUE self, int opt, VALUE value) {
  *
  * Returns a string that represents the client library version.
  */
-static VALUE rb_mysql_client_info(VALUE self) {
-  VALUE version, client_info;
-#ifdef HAVE_RUBY_ENCODING_H
-  rb_encoding *default_internal_enc;
-  rb_encoding *conn_enc;
-  GET_CLIENT(self);
-#endif
-  version = rb_hash_new();
+static VALUE rb_mysql_client_info(RB_MYSQL_UNUSED VALUE klass) {
+  VALUE version_info, version, header_version;
+  version_info = rb_hash_new();
 
-#ifdef HAVE_RUBY_ENCODING_H
-  default_internal_enc = rb_default_internal_encoding();
-  conn_enc = rb_to_encoding(wrapper->encoding);
-#endif
+  version = rb_str_new2(mysql_get_client_info());
+  header_version = rb_str_new2(MYSQL_LINK_VERSION);
 
-  rb_hash_aset(version, sym_id, LONG2NUM(mysql_get_client_version()));
-  client_info = rb_str_new2(mysql_get_client_info());
 #ifdef HAVE_RUBY_ENCODING_H
-  rb_enc_associate(client_info, conn_enc);
-  if (default_internal_enc) {
-    client_info = rb_str_export_to_enc(client_info, default_internal_enc);
-  }
+  rb_enc_associate(version, rb_usascii_encoding());
+  rb_enc_associate(header_version, rb_usascii_encoding());
 #endif
-  rb_hash_aset(version, sym_version, client_info);
-  return version;
+
+  rb_hash_aset(version_info, sym_id, LONG2NUM(mysql_get_client_version()));
+  rb_hash_aset(version_info, sym_version, version);
+  rb_hash_aset(version_info, sym_header_version, header_version);
+
+  return version_info;
 }
 
 /* call-seq:
@@ -906,19 +892,17 @@ static VALUE rb_mysql_client_server_info(VALUE self) {
  *
  * Return the file descriptor number for this client.
  */
+#ifndef _WIN32
 static VALUE rb_mysql_client_socket(VALUE self) {
   GET_CLIENT(self);
-#ifndef _WIN32
-  {
-    int fd_set_fd;
-    REQUIRE_CONNECTED(wrapper);
-    fd_set_fd = wrapper->client->net.fd;
-    return INT2NUM(fd_set_fd);
-  }
+  REQUIRE_CONNECTED(wrapper);
+  return INT2NUM(wrapper->client->net.fd);
+}
 #else
+static VALUE rb_mysql_client_socket(RB_MYSQL_UNUSED VALUE self) {
   rb_raise(cMysql2Error, "Raw access to the mysql file descriptor isn't supported on Windows");
-#endif
 }
+#endif
 
 /* call-seq:
  *    client.last_id
@@ -987,7 +971,7 @@ static VALUE rb_mysql_client_select_db(VALUE self, VALUE db)
   REQUIRE_CONNECTED(wrapper);
 
   args.mysql = wrapper->client;
-  args.db = StringValuePtr(db);
+  args.db = StringValueCStr(db);
 
   if (rb_thread_call_without_gvl(nogvl_select_db, &args, RUBY_UBF_IO, 0) == Qfalse)
     rb_raise_mysql2_error(wrapper);
@@ -1078,9 +1062,9 @@ static VALUE rb_mysql_client_store_result(VALUE self)
   }
 
   current = rb_hash_dup(rb_iv_get(self, "@current_query_options"));
-  RB_GC_GUARD(current);
+  (void)RB_GC_GUARD(current);
   Check_Type(current, T_HASH);
-  resultObj = rb_mysql_result_to_obj(self, wrapper->encoding, current, result);
+  resultObj = rb_mysql_result_to_obj(self, wrapper->encoding, current, result, Qnil);
 
   return resultObj;
 }
@@ -1149,7 +1133,6 @@ static VALUE set_write_timeout(VALUE self, VALUE value) {
 static VALUE set_charset_name(VALUE self, VALUE value) {
   char *charset_name;
 #ifdef HAVE_RUBY_ENCODING_H
-  size_t charset_name_len;
   const struct mysql2_mysql_enc_name_to_rb_map *mysql2rb;
   rb_encoding *enc;
   VALUE rb_enc;
@@ -1159,8 +1142,7 @@ static VALUE set_charset_name(VALUE self, VALUE value) {
   charset_name = RSTRING_PTR(value);
 
 #ifdef HAVE_RUBY_ENCODING_H
-  charset_name_len = RSTRING_LEN(value);
-  mysql2rb = mysql2_mysql_enc_name_to_rb(charset_name, charset_name_len);
+  mysql2rb = mysql2_mysql_enc_name_to_rb(charset_name, (unsigned int)RSTRING_LEN(value));
   if (mysql2rb == NULL || mysql2rb->rb_name == NULL) {
     VALUE inspect = rb_inspect(value);
     rb_raise(cMysql2Error, "Unsupported charset: '%s'", RSTRING_PTR(inspect));
@@ -1183,11 +1165,11 @@ static VALUE set_ssl_options(VALUE self, VALUE key, VALUE cert, VALUE ca, VALUE
   GET_CLIENT(self);
 
   mysql_ssl_set(wrapper->client,
-      NIL_P(key) ? NULL : StringValuePtr(key),
-      NIL_P(cert) ? NULL : StringValuePtr(cert),
-      NIL_P(ca) ? NULL : StringValuePtr(ca),
-      NIL_P(capath) ? NULL : StringValuePtr(capath),
-      NIL_P(cipher) ? NULL : StringValuePtr(cipher));
+      NIL_P(key)    ? NULL : StringValueCStr(key),
+      NIL_P(cert)   ? NULL : StringValueCStr(cert),
+      NIL_P(ca)     ? NULL : StringValueCStr(ca),
+      NIL_P(capath) ? NULL : StringValueCStr(capath),
+      NIL_P(cipher) ? NULL : StringValueCStr(cipher));
 
   return self;
 }
@@ -1220,7 +1202,19 @@ static VALUE initialize_ext(VALUE self) {
   return self;
 }
 
+/* call-seq: client.prepare # => Mysql2::Statement
+ *
+ * Create a new prepared statement.
+ */
+static VALUE rb_mysql_client_prepare_statement(VALUE self, VALUE sql) {
+  GET_CLIENT(self);
+  REQUIRE_CONNECTED(wrapper);
+
+  return rb_mysql_stmt_new(self, sql);
+}
+
 void init_mysql2_client() {
+#ifdef _WIN32
   /* verify the libmysql we're about to use was the version we were built against
      https://github.com/luislavena/mysql-gem/commit/a600a9c459597da0712f70f43736e24b484f8a99 */
   int i;
@@ -1235,15 +1229,14 @@ void init_mysql2_client() {
     }
     if (lib[i] != MYSQL_LINK_VERSION[i]) {
       rb_raise(rb_eRuntimeError, "Incorrect MySQL client library version! This gem was compiled for %s but the client library is %s.", MYSQL_LINK_VERSION, lib);
-      return;
     }
   }
+#endif
 
   /* Initializing mysql library, so different threads could call Client.new */
   /* without race condition in the library */
   if (mysql_library_init(0, NULL, NULL) != 0) {
     rb_raise(rb_eRuntimeError, "Could not initialize MySQL client library");
-    return;
   }
 
 #if 0
@@ -1254,17 +1247,17 @@ void init_mysql2_client() {
   rb_define_alloc_func(cMysql2Client, allocate);
 
   rb_define_singleton_method(cMysql2Client, "escape", rb_mysql_client_escape, 1);
+  rb_define_singleton_method(cMysql2Client, "info", rb_mysql_client_info, 0);
 
   rb_define_method(cMysql2Client, "close", rb_mysql_client_close, 0);
-  rb_define_method(cMysql2Client, "query", rb_mysql_client_query, -1);
   rb_define_method(cMysql2Client, "abandon_results!", rb_mysql_client_abandon_results, 0);
   rb_define_method(cMysql2Client, "escape", rb_mysql_client_real_escape, 1);
-  rb_define_method(cMysql2Client, "info", rb_mysql_client_info, 0);
   rb_define_method(cMysql2Client, "server_info", rb_mysql_client_server_info, 0);
   rb_define_method(cMysql2Client, "socket", rb_mysql_client_socket, 0);
   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);
@@ -1290,19 +1283,21 @@ void init_mysql2_client() {
   rb_define_private_method(cMysql2Client, "ssl_set", set_ssl_options, 5);
   rb_define_private_method(cMysql2Client, "initialize_ext", initialize_ext, 0);
   rb_define_private_method(cMysql2Client, "connect", rb_connect, 7);
+  rb_define_private_method(cMysql2Client, "_query", rb_query, 2);
 
   sym_id              = ID2SYM(rb_intern("id"));
   sym_version         = ID2SYM(rb_intern("version"));
+  sym_header_version  = ID2SYM(rb_intern("header_version"));
   sym_async           = ID2SYM(rb_intern("async"));
   sym_symbolize_keys  = ID2SYM(rb_intern("symbolize_keys"));
   sym_as              = ID2SYM(rb_intern("as"));
   sym_array           = ID2SYM(rb_intern("array"));
   sym_stream          = ID2SYM(rb_intern("stream"));
 
+  intern_brackets = rb_intern("[]");
   intern_merge = rb_intern("merge");
   intern_merge_bang = rb_intern("merge!");
-  intern_error_number_eql = rb_intern("error_number=");
-  intern_sql_state_eql = rb_intern("sql_state=");
+  intern_new_with_args = rb_intern("new_with_args");
 
 #ifdef CLIENT_LONG_PASSWORD
   rb_const_set(cMysql2Client, rb_intern("LONG_PASSWORD"),
diff --git a/ext/mysql2/client.h b/ext/mysql2/client.h
index f7af5de..3e29d60 100644
--- a/ext/mysql2/client.h
+++ b/ext/mysql2/client.h
@@ -50,7 +50,27 @@ typedef struct {
   MYSQL *client;
 } mysql_client_wrapper;
 
-void init_mysql2_client();
+#define REQUIRE_CONNECTED(wrapper) \
+  REQUIRE_INITIALIZED(wrapper) \
+  if (!wrapper->connected && !wrapper->reconnect_enabled) { \
+    rb_raise(cMysql2Error, "closed MySQL connection"); \
+  }
+
+void rb_mysql_client_set_active_thread(VALUE self);
+
+#define MARK_CONN_INACTIVE(conn) do {\
+    wrapper->active_thread = Qnil; \
+  } while(0)
+
+#define GET_CLIENT(self) \
+  mysql_client_wrapper *wrapper; \
+  Data_Get_Struct(self, mysql_client_wrapper, wrapper);
+
+void init_mysql2_client(void);
 void decr_mysql2_client(mysql_client_wrapper *wrapper);
 
 #endif
+
+#ifndef HAVE_RB_HASH_DUP
+VALUE rb_hash_dup(VALUE other);
+#endif
diff --git a/ext/mysql2/extconf.rb b/ext/mysql2/extconf.rb
index ca0e2c8..ed82355 100644
--- a/ext/mysql2/extconf.rb
+++ b/ext/mysql2/extconf.rb
@@ -1,8 +1,15 @@
 # encoding: UTF-8
 require 'mkmf'
+require 'English'
 
-def asplode lib
-  abort "-----\n#{lib} is missing.  please check your installation of mysql and try again.\n-----"
+def asplode(lib)
+  if RUBY_PLATFORM =~ /mingw|mswin/
+    abort "-----\n#{lib} is missing. Check your installation of MySQL or Connector/C, and try again.\n-----"
+  elsif RUBY_PLATFORM =~ /darwin/
+    abort "-----\n#{lib} is missing. You may need to 'brew install mysql' or 'port install mysql', and try again.\n-----"
+  else
+    abort "-----\n#{lib} is missing. You may need to 'apt-get install libmysqlclient-dev' or 'yum install mysql-devel', and try again.\n-----"
+  end
 end
 
 # 2.0-only
@@ -16,7 +23,7 @@ have_func('rb_intern3')
 
 # borrowed from mysqlplus
 # http://github.com/oldmoe/mysqlplus/blob/master/ext/extconf.rb
-dirs = ENV['PATH'].split(File::PATH_SEPARATOR) + %w[
+dirs = ENV.fetch('PATH').split(File::PATH_SEPARATOR) + %w(
   /opt
   /opt/local
   /opt/local/mysql
@@ -27,13 +34,15 @@ dirs = ENV['PATH'].split(File::PATH_SEPARATOR) + %w[
   /usr/local/mysql
   /usr/local/mysql-*
   /usr/local/lib/mysql5*
-].map{|dir| "#{dir}/bin" }
+  /usr/local/opt/mysql5*
+).map { |dir| dir << '/bin' }
 
-GLOB = "{#{dirs.join(',')}}/{mysql_config,mysql_config5}"
+GLOB = "{#{dirs.join(',')}}/{mysql_config,mysql_config5,mariadb_config}"
 
 # If the user has provided a --with-mysql-dir argument, we must respect it or fail.
 inc, lib = dir_config('mysql')
 if inc && lib
+  # TODO: Remove when 2.0.0 is the minimum supported version
   # Ruby versions not incorporating the mkmf fix at
   # https://bugs.ruby-lang.org/projects/ruby-trunk/repository/revisions/39717
   # do not properly search for lib directories, and must be corrected
@@ -41,37 +50,33 @@ if inc && lib
     @libdir_basename = 'lib'
     inc, lib = dir_config('mysql')
   end
-  abort "-----\nCannot find include dir(s) #{inc}\n-----" unless inc && inc.split(File::PATH_SEPARATOR).any?{|dir| File.directory?(dir)}
-  abort "-----\nCannot find library dir(s) #{lib}\n-----" unless lib && lib.split(File::PATH_SEPARATOR).any?{|dir| File.directory?(dir)}
-  warn  "-----\nUsing --with-mysql-dir=#{File.dirname inc}\n-----"
+  abort "-----\nCannot find include dir(s) #{inc}\n-----" unless inc && inc.split(File::PATH_SEPARATOR).any? { |dir| File.directory?(dir) }
+  abort "-----\nCannot find library dir(s) #{lib}\n-----" unless lib && lib.split(File::PATH_SEPARATOR).any? { |dir| File.directory?(dir) }
+  warn "-----\nUsing --with-mysql-dir=#{File.dirname inc}\n-----"
   rpath_dir = lib
-elsif mc = (with_config('mysql-config') || Dir[GLOB].first)
+elsif (mc = (with_config('mysql-config') || Dir[GLOB].first))
   # If the user has provided a --with-mysql-config argument, we must respect it or fail.
   # If the user gave --with-mysql-config with no argument means we should try to find it.
   mc = Dir[GLOB].first if mc == true
-  abort "-----\nCannot find mysql_config at #{mc}\n-----" unless mc && File.exists?(mc)
+  abort "-----\nCannot find mysql_config at #{mc}\n-----" unless mc && File.exist?(mc)
   abort "-----\nCannot execute mysql_config at #{mc}\n-----" unless File.executable?(mc)
-  warn  "-----\nUsing mysql_config at #{mc}\n-----"
+  warn "-----\nUsing mysql_config at #{mc}\n-----"
   ver = `#{mc} --version`.chomp.to_f
   includes = `#{mc} --include`.chomp
-  exit 1 if $? != 0
+  abort unless $CHILD_STATUS.success?
   libs = `#{mc} --libs_r`.chomp
   # MySQL 5.5 and above already have re-entrant code in libmysqlclient (no _r).
-  if ver >= 5.5 || libs.empty?
-    libs = `#{mc} --libs`.chomp
-  end
-  exit 1 if $? != 0
+  libs = `#{mc} --libs`.chomp if ver >= 5.5 || libs.empty?
+  abort unless $CHILD_STATUS.success?
   $INCFLAGS += ' ' + includes
   $libs = libs + " " + $libs
   rpath_dir = libs
 else
-  inc, lib = dir_config('mysql', '/usr/local')
-  libs = ['m', 'z', 'socket', 'nsl', 'mygcc']
-  while not find_library('mysqlclient', 'mysql_query', lib, "#{lib}/mysql") do
-    exit 1 if libs.empty?
-    have_library(libs.shift)
-  end
-  rpath_dir = lib
+  _, usr_local_lib = dir_config('mysql', '/usr/local')
+
+  asplode("mysql client") unless find_library('mysqlclient', 'mysql_query', usr_local_lib, "#{usr_local_lib}/mysql")
+
+  rpath_dir = usr_local_lib
 end
 
 if have_header('mysql.h')
@@ -82,16 +87,71 @@ else
   asplode 'mysql.h'
 end
 
-%w{ errmsg.h mysqld_error.h }.each do |h|
+%w(errmsg.h mysqld_error.h).each do |h|
   header = [prefix, h].compact.join '/'
-  asplode h unless have_header h
+  asplode h unless have_header header
+end
+
+# This is our wishlist. We use whichever flags work on the host.
+# -Wall and -Wextra are included by default.
+wishlist = [
+  '-Weverything',
+  '-Wno-bad-function-cast', # rb_thread_call_without_gvl returns void * that we cast to VALUE
+  '-Wno-conditional-uninitialized', # false positive in client.c
+  '-Wno-covered-switch-default', # result.c -- enum_field_types (when fully covered, e.g. mysql 5.5)
+  '-Wno-declaration-after-statement', # GET_CLIENT followed by GET_STATEMENT in statement.c
+  '-Wno-disabled-macro-expansion', # rubby :(
+  '-Wno-documentation-unknown-command', # rubby :(
+  '-Wno-missing-field-initializers', # gperf generates bad code
+  '-Wno-missing-variable-declarations', # missing symbols due to ruby native ext initialization
+  '-Wno-padded', # mysql :(
+  '-Wno-reserved-id-macro', # rubby :(
+  '-Wno-sign-conversion', # gperf generates bad code
+  '-Wno-static-in-inline', # gperf generates bad code
+  '-Wno-switch-enum', # result.c -- enum_field_types (when not fully covered, e.g. mysql 5.6+)
+  '-Wno-undef', # rubinius :(
+  '-Wno-unreachable-code', # rubby :(
+  '-Wno-used-but-marked-unused', # rubby :(
+]
+
+usable_flags = wishlist.select do |flag|
+  try_link('int main() {return 0;}',  "-Werror #{flag}")
 end
 
-# These gcc style flags are also supported by clang and xcode compilers,
-# so we'll use a does-it-work test instead of an is-it-gcc test.
-gcc_flags = ' -Wall -funroll-loops'
-if try_link('int main() {return 0;}', gcc_flags)
-  $CFLAGS << gcc_flags
+$CFLAGS << ' ' << usable_flags.join(' ')
+
+enabled_sanitizers = disabled_sanitizers = []
+# Specify a commna-separated list of sanitizers, or try them all by default
+sanitizers = with_config('sanitize')
+case sanitizers
+when true
+  # Try them all, turn on whatever we can
+  enabled_sanitizers = %w(address cfi integer memory thread undefined).select do |s|
+    try_link('int main() {return 0;}',  "-Werror -fsanitize=#{s}")
+  end
+  abort "-----\nCould not enable any sanitizers!\n-----" if enabled_sanitizers.empty?
+when String
+  # Figure out which sanitizers are supported
+  enabled_sanitizers, disabled_sanitizers = sanitizers.split(',').partition do |s|
+    try_link('int main() {return 0;}',  "-Werror -fsanitize=#{s}")
+  end
+end
+
+unless disabled_sanitizers.empty?
+  abort "-----\nCould not enable requested sanitizers: #{disabled_sanitizers.join(',')}\n-----"
+end
+
+unless enabled_sanitizers.empty?
+  warn "-----\nEnabling sanitizers: #{enabled_sanitizers.join(',')}\n-----"
+  enabled_sanitizers.each do |s|
+    # address sanitizer requires runtime support
+    if s == 'address' # rubocop:disable Style/IfUnlessModifier
+      have_library('asan') || $LDFLAGS << ' -fsanitize=address'
+    end
+    $CFLAGS << " -fsanitize=#{s}"
+  end
+  # Options for line numbers in backtraces
+  $CFLAGS << ' -g -fno-omit-frame-pointer'
 end
 
 if RUBY_PLATFORM =~ /mswin|mingw/
@@ -102,7 +162,7 @@ if RUBY_PLATFORM =~ /mswin|mingw/
   # Use rake to rebuild only if these files change
   deffile = File.expand_path('../../../support/libmysql.def', __FILE__)
   libfile = File.expand_path(File.join(rpath_dir, 'libmysql.lib'))
-  file 'libmysql.a' => [deffile, libfile] do |t|
+  file 'libmysql.a' => [deffile, libfile] do
     when_writing 'building libmysql.a' do
       # Ruby kindly shows us where dllwrap is, but that tool does more than we want.
       # Maybe in the future Ruby could provide RbConfig::CONFIG['DLLTOOL'] directly.
@@ -119,8 +179,8 @@ if RUBY_PLATFORM =~ /mswin|mingw/
 
   # Make sure the generated interface library works (if cross-compiling, trust without verifying)
   unless RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
-    abort "-----\nCannot find libmysql.a\n----" unless have_library('libmysql')
-    abort "-----\nCannot link to libmysql.a (my_init)\n----" unless have_func('my_init')
+    abort "-----\nCannot find libmysql.a\n-----" unless have_library('libmysql')
+    abort "-----\nCannot link to libmysql.a (my_init)\n-----" unless have_func('my_init')
   end
 
   # Vendor libmysql.dll
@@ -129,7 +189,7 @@ if RUBY_PLATFORM =~ /mswin|mingw/
 
   vendordll = File.join(vendordir, 'libmysql.dll')
   dllfile = File.expand_path(File.join(rpath_dir, 'libmysql.dll'))
-  file vendordll => [dllfile, vendordir] do |t|
+  file vendordll => [dllfile, vendordir] do
     when_writing 'copying libmysql.dll' do
       cp dllfile, vendordll
     end
@@ -155,7 +215,7 @@ else
     warn "-----\nSetting mysql rpath to #{explicit_rpath}\n-----"
     $LDFLAGS << rpath_flags
   else
-    if libdir = rpath_dir[%r{(-L)?(/[^ ]+)}, 2]
+    if (libdir = rpath_dir[%r{(-L)?(/[^ ]+)}, 2])
       rpath_flags = " -Wl,-rpath,#{libdir}"
       if RbConfig::CONFIG["RPATHFLAG"].to_s.empty? && try_link('int main() {return 0;}', rpath_flags)
         # Usually Ruby sets RPATHFLAG the right way for each system, but not on OS X.
diff --git a/ext/mysql2/infile.c b/ext/mysql2/infile.c
index 6ff1f1f..b25934d 100644
--- a/ext/mysql2/infile.c
+++ b/ext/mysql2/infile.c
@@ -56,7 +56,7 @@ mysql2_local_infile_init(void **ptr, const char *filename, void *userdata)
  * < 0   error
  */
 static int
-mysql2_local_infile_read(void *ptr, char *buf, uint buf_len)
+mysql2_local_infile_read(void *ptr, char *buf, unsigned int buf_len)
 {
   int count;
   mysql2_local_infile_data *data = (mysql2_local_infile_data *)ptr;
@@ -95,7 +95,7 @@ mysql2_local_infile_end(void *ptr)
  * Error message number (see http://dev.mysql.com/doc/refman/5.0/en/error-messages-client.html)
  */
 static int
-mysql2_local_infile_error(void *ptr, char *error_msg, uint error_msg_len)
+mysql2_local_infile_error(void *ptr, char *error_msg, unsigned int error_msg_len)
 {
   mysql2_local_infile_data *data = (mysql2_local_infile_data *) ptr;
 
diff --git a/ext/mysql2/mysql2_ext.c b/ext/mysql2/mysql2_ext.c
index dcb72f3..a6fe365 100644
--- a/ext/mysql2/mysql2_ext.c
+++ b/ext/mysql2/mysql2_ext.c
@@ -9,4 +9,5 @@ void Init_mysql2() {
 
   init_mysql2_client();
   init_mysql2_result();
+  init_mysql2_statement();
 }
diff --git a/ext/mysql2/mysql2_ext.h b/ext/mysql2/mysql2_ext.h
index d6d5fd6..f53f4b4 100644
--- a/ext/mysql2/mysql2_ext.h
+++ b/ext/mysql2/mysql2_ext.h
@@ -1,18 +1,14 @@
 #ifndef MYSQL2_EXT
 #define MYSQL2_EXT
 
+void Init_mysql2(void);
+
 /* tell rbx not to use it's caching compat layer
    by doing this we're making a promise to RBX that
    we'll never modify the pointers we get back from RSTRING_PTR */
 #define RSTRING_NOT_MODIFIED
 #include <ruby.h>
 
-#ifndef HAVE_UINT
-#define HAVE_UINT
-typedef unsigned short    ushort;
-typedef unsigned int    uint;
-#endif
-
 #ifdef HAVE_MYSQL_H
 #include <mysql.h>
 #include <mysql_com.h>
@@ -33,12 +29,15 @@ typedef unsigned int    uint;
 #endif
 
 #if defined(__GNUC__) && (__GNUC__ >= 3)
+#define RB_MYSQL_NORETURN __attribute__ ((noreturn))
 #define RB_MYSQL_UNUSED __attribute__ ((unused))
 #else
+#define RB_MYSQL_NORETURN
 #define RB_MYSQL_UNUSED
 #endif
 
 #include <client.h>
+#include <statement.h>
 #include <result.h>
 #include <infile.h>
 
diff --git a/ext/mysql2/mysql_enc_name_to_ruby.h b/ext/mysql2/mysql_enc_name_to_ruby.h
index dfabeef..9542e22 100644
--- a/ext/mysql2/mysql_enc_name_to_ruby.h
+++ b/ext/mysql2/mysql_enc_name_to_ruby.h
@@ -1,4 +1,4 @@
-/* C code produced by gperf version 3.0.3 */
+/* C code produced by gperf version 3.0.4 */
 /* Command-line: gperf  */
 /* Computed positions: -k'1,3,$' */
 
@@ -78,7 +78,7 @@ mysql2_mysql_enc_name_to_rb_hash (str, len)
 
 #ifdef __GNUC__
 __inline
-#ifdef __GNUC_STDC_INLINE__
+#if defined __GNUC_STDC_INLINE__ || defined __GNUC_GNU_INLINE__
 __attribute__ ((__gnu_inline__))
 #endif
 #endif
diff --git a/ext/mysql2/mysql_enc_to_ruby.h b/ext/mysql2/mysql_enc_to_ruby.h
index 37dbf6f..df167b2 100644
--- a/ext/mysql2/mysql_enc_to_ruby.h
+++ b/ext/mysql2/mysql_enc_to_ruby.h
@@ -1,4 +1,4 @@
-const char *mysql2_mysql_enc_to_rb[] = {
+static const char *mysql2_mysql_enc_to_rb[] = {
   "Big5",
   "ISO-8859-2",
   NULL,
@@ -54,13 +54,13 @@ const char *mysql2_mysql_enc_to_rb[] = {
   "macRoman",
   "UTF-16",
   "UTF-16",
-  NULL,
+  "",
   "Windows-1256",
   "Windows-1257",
   "Windows-1257",
   "UTF-32",
   "UTF-32",
-  NULL,
+  "",
   "ASCII-8BIT",
   NULL,
   "US-ASCII",
@@ -119,10 +119,10 @@ const char *mysql2_mysql_enc_to_rb[] = {
   "UTF-16",
   "UTF-16",
   "UTF-16",
-  NULL,
-  NULL,
-  NULL,
-  NULL,
+  "UTF-16",
+  "UTF-16",
+  "UTF-16",
+  "UTF-16",
   NULL,
   NULL,
   NULL,
@@ -146,6 +146,10 @@ const char *mysql2_mysql_enc_to_rb[] = {
   "UTF-16BE",
   "UTF-16BE",
   "UTF-16BE",
+  "UTF-16BE",
+  "UTF-16BE",
+  "UTF-16BE",
+  "UTF-16BE",
   NULL,
   NULL,
   NULL,
@@ -153,11 +157,11 @@ const char *mysql2_mysql_enc_to_rb[] = {
   NULL,
   NULL,
   NULL,
-  NULL,
-  NULL,
-  NULL,
-  NULL,
-  NULL,
+  "UTF-16BE",
+  "UTF-32",
+  "UTF-32",
+  "UTF-32",
+  "UTF-32",
   "UTF-32",
   "UTF-32",
   "UTF-32",
@@ -178,10 +182,6 @@ const char *mysql2_mysql_enc_to_rb[] = {
   "UTF-32",
   "UTF-32",
   "UTF-32",
-  NULL,
-  NULL,
-  NULL,
-  NULL,
   NULL,
   NULL,
   NULL,
@@ -210,6 +210,10 @@ const char *mysql2_mysql_enc_to_rb[] = {
   "UTF-8",
   "UTF-8",
   "UTF-8",
+  "UTF-8",
+  "UTF-8",
+  "UTF-8",
+  "UTF-8",
   NULL,
   NULL,
   NULL,
@@ -217,11 +221,11 @@ const char *mysql2_mysql_enc_to_rb[] = {
   NULL,
   NULL,
   NULL,
-  NULL,
-  NULL,
-  NULL,
-  NULL,
-  NULL,
+  "UTF-8",
+  "UTF-8",
+  "UTF-8",
+  "UTF-8",
+  "UTF-8",
   "UTF-8",
   "UTF-8",
   "UTF-8",
@@ -243,4 +247,3 @@ const char *mysql2_mysql_enc_to_rb[] = {
   "UTF-8",
   "UTF-8"
 };
-
diff --git a/ext/mysql2/result.c b/ext/mysql2/result.c
index 54dbc13..040e9d5 100644
--- a/ext/mysql2/result.c
+++ b/ext/mysql2/result.c
@@ -1,7 +1,5 @@
 #include <mysql2_ext.h>
 
-#include <stdint.h>
-
 #include "mysql_enc_to_ruby.h"
 
 #ifdef HAVE_RUBY_ENCODING_H
@@ -50,8 +48,24 @@ static rb_encoding *binaryEncoding;
 #define MYSQL2_MIN_TIME 62171150401ULL
 #endif
 
+#define GET_RESULT(self) \
+  mysql2_result_wrapper *wrapper; \
+  Data_Get_Struct(self, mysql2_result_wrapper, wrapper);
+
+typedef struct {
+  int symbolizeKeys;
+  int asArray;
+  int castBool;
+  int cacheRows;
+  int cast;
+  int streaming;
+  ID db_timezone;
+  ID app_timezone;
+  VALUE block_given;
+} result_each_args;
+
+VALUE cBigDecimal, cDateTime, cDate;
 static VALUE cMysql2Result;
-static VALUE cBigDecimal, cDate, cDateTime;
 static VALUE opt_decimal_zero, opt_float_zero, opt_time_year, opt_time_month, opt_utc_offset;
 extern VALUE mMysql2, cMysql2Client, cMysql2Error;
 static ID intern_new, intern_utc, intern_local, intern_localtime, intern_local_offset, intern_civil, intern_new_offset;
@@ -59,6 +73,7 @@ static VALUE sym_symbolize_keys, sym_as, sym_array, sym_database_timezone, sym_a
           sym_local, sym_utc, sym_cast_booleans, sym_cache_rows, sym_cast, sym_stream, sym_name;
 static ID intern_merge;
 
+/* Mark any VALUEs that are only referenced in C, so the GC won't get them. */
 static void rb_mysql_result_mark(void * wrapper) {
   mysql2_result_wrapper * w = wrapper;
   if (w) {
@@ -66,13 +81,46 @@ static void rb_mysql_result_mark(void * wrapper) {
     rb_gc_mark(w->rows);
     rb_gc_mark(w->encoding);
     rb_gc_mark(w->client);
+    rb_gc_mark(w->statement);
   }
 }
 
 /* this may be called manually or during GC */
 static void rb_mysql_result_free_result(mysql2_result_wrapper * wrapper) {
-  if (wrapper && wrapper->resultFreed != 1) {
+  if (!wrapper) return;
+
+  if (wrapper->resultFreed != 1) {
+    if (wrapper->stmt_wrapper) {
+      if (!wrapper->stmt_wrapper->closed) {
+        mysql_stmt_free_result(wrapper->stmt_wrapper->stmt);
+
+        /* MySQL BUG? If the statement handle was previously used, and so
+         * mysql_stmt_bind_result was called, and if that result set and bind buffers were freed,
+         * MySQL still thinks the result set buffer is available and will prefetch the
+         * first result in mysql_stmt_execute. This will corrupt or crash the program.
+         * By setting bind_result_done back to 0, we make MySQL think that a result set
+         * has never been bound to this statement handle before to prevent the prefetch.
+         */
+        wrapper->stmt_wrapper->stmt->bind_result_done = 0;
+      }
+
+      if (wrapper->result_buffers) {
+        unsigned int i;
+        for (i = 0; i < wrapper->numberOfFields; i++) {
+          if (wrapper->result_buffers[i].buffer) {
+            xfree(wrapper->result_buffers[i].buffer);
+          }
+        }
+        xfree(wrapper->result_buffers);
+        xfree(wrapper->is_null);
+        xfree(wrapper->error);
+        xfree(wrapper->length);
+      }
+      /* Clue that the next statement execute will need to allocate a new result buffer. */
+      wrapper->result_buffers = NULL;
+    }
     /* FIXME: this may call flush_use_result, which can hit the socket */
+    /* For prepared statements, wrapper->result is the result metadata */
     mysql_free_result(wrapper->result);
     wrapper->resultFreed = 1;
   }
@@ -80,7 +128,7 @@ static void rb_mysql_result_free_result(mysql2_result_wrapper * wrapper) {
 
 /* this is called during GC */
 static void rb_mysql_result_free(void *ptr) {
-  mysql2_result_wrapper * wrapper = ptr;
+  mysql2_result_wrapper *wrapper = ptr;
   rb_mysql_result_free_result(wrapper);
 
   // If the GC gets to client first it will be nil
@@ -88,6 +136,10 @@ static void rb_mysql_result_free(void *ptr) {
     decr_mysql2_client(wrapper->client_wrapper);
   }
 
+  if (wrapper->statement != Qnil) {
+    decr_mysql2_stmt(wrapper->stmt_wrapper);
+  }
+
   xfree(wrapper);
 }
 
@@ -102,10 +154,16 @@ static void *nogvl_fetch_row(void *ptr) {
   return mysql_fetch_row(result);
 }
 
-static VALUE rb_mysql_result_fetch_field(VALUE self, unsigned int idx, short int symbolize_keys) {
-  mysql2_result_wrapper * wrapper;
+static void *nogvl_stmt_fetch(void *ptr) {
+  MYSQL_STMT *stmt = ptr;
+  uintptr_t r = mysql_stmt_fetch(stmt);
+
+  return (void *)r;
+}
+
+static VALUE rb_mysql_result_fetch_field(VALUE self, unsigned int idx, int symbolize_keys) {
   VALUE rb_field;
-  GetMysql2Result(self, wrapper);
+  GET_RESULT(self);
 
   if (wrapper->fields == Qnil) {
     wrapper->numberOfFields = mysql_num_fields(wrapper->result);
@@ -147,7 +205,7 @@ static VALUE rb_mysql_result_fetch_field(VALUE self, unsigned int idx, short int
 
 #ifdef HAVE_RUBY_ENCODING_H
 static VALUE mysql2_set_field_string_encoding(VALUE val, MYSQL_FIELD field, rb_encoding *default_internal_enc, rb_encoding *conn_enc) {
-  /* if binary flag is set, respect it's wishes */
+  /* if binary flag is set, respect its wishes */
   if (field.flags & BINARY_FLAG && field.charsetnr == 63) {
     rb_enc_associate(val, binaryEncoding);
   } else if (!field.charsetnr) {
@@ -182,7 +240,7 @@ static VALUE mysql2_set_field_string_encoding(VALUE val, MYSQL_FIELD field, rb_e
  */
 static unsigned int msec_char_to_uint(char *msec_char, size_t len)
 {
-  int i;
+  size_t i;
   for (i = 0; i < (len - 1); i++) {
     if (msec_char[i] == '\0') {
       msec_char[i] = '0';
@@ -191,9 +249,272 @@ static unsigned int msec_char_to_uint(char *msec_char, size_t len)
   return (unsigned int)strtoul(msec_char, NULL, 10);
 }
 
-static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezone, int symbolizeKeys, int asArray, int castBool, int cast, MYSQL_FIELD * fields) {
+static void rb_mysql_result_alloc_result_buffers(VALUE self, MYSQL_FIELD *fields) {
+  unsigned int i;
+  GET_RESULT(self);
+
+  if (wrapper->result_buffers != NULL) return;
+
+  wrapper->result_buffers = xcalloc(wrapper->numberOfFields, sizeof(MYSQL_BIND));
+  wrapper->is_null = xcalloc(wrapper->numberOfFields, sizeof(my_bool));
+  wrapper->error = xcalloc(wrapper->numberOfFields, sizeof(my_bool));
+  wrapper->length = xcalloc(wrapper->numberOfFields, sizeof(unsigned long));
+
+  for (i = 0; i < wrapper->numberOfFields; i++) {
+    wrapper->result_buffers[i].buffer_type = fields[i].type;
+
+    //      mysql type    |            C type
+    switch(fields[i].type) {
+      case MYSQL_TYPE_NULL:         // NULL
+        break;
+      case MYSQL_TYPE_TINY:         // signed char
+        wrapper->result_buffers[i].buffer = xcalloc(1, sizeof(signed char));
+        wrapper->result_buffers[i].buffer_length = sizeof(signed char);
+        break;
+      case MYSQL_TYPE_SHORT:        // short int
+        wrapper->result_buffers[i].buffer = xcalloc(1, sizeof(short int));
+        wrapper->result_buffers[i].buffer_length = sizeof(short int);
+        break;
+      case MYSQL_TYPE_INT24:        // int
+      case MYSQL_TYPE_LONG:         // int
+      case MYSQL_TYPE_YEAR:         // int
+        wrapper->result_buffers[i].buffer = xcalloc(1, sizeof(int));
+        wrapper->result_buffers[i].buffer_length = sizeof(int);
+        break;
+      case MYSQL_TYPE_LONGLONG:     // long long int
+        wrapper->result_buffers[i].buffer = xcalloc(1, sizeof(long long int));
+        wrapper->result_buffers[i].buffer_length = sizeof(long long int);
+        break;
+      case MYSQL_TYPE_FLOAT:        // float
+      case MYSQL_TYPE_DOUBLE:       // double
+        wrapper->result_buffers[i].buffer = xcalloc(1, sizeof(double));
+        wrapper->result_buffers[i].buffer_length = sizeof(double);
+        break;
+      case MYSQL_TYPE_TIME:         // MYSQL_TIME
+      case MYSQL_TYPE_DATE:         // MYSQL_TIME
+      case MYSQL_TYPE_NEWDATE:      // MYSQL_TIME
+      case MYSQL_TYPE_DATETIME:     // MYSQL_TIME
+      case MYSQL_TYPE_TIMESTAMP:    // MYSQL_TIME
+        wrapper->result_buffers[i].buffer = xcalloc(1, sizeof(MYSQL_TIME));
+        wrapper->result_buffers[i].buffer_length = sizeof(MYSQL_TIME);
+        break;
+      case MYSQL_TYPE_DECIMAL:      // char[]
+      case MYSQL_TYPE_NEWDECIMAL:   // char[]
+      case MYSQL_TYPE_STRING:       // char[]
+      case MYSQL_TYPE_VAR_STRING:   // char[]
+      case MYSQL_TYPE_VARCHAR:      // char[]
+      case MYSQL_TYPE_TINY_BLOB:    // char[]
+      case MYSQL_TYPE_BLOB:         // char[]
+      case MYSQL_TYPE_MEDIUM_BLOB:  // char[]
+      case MYSQL_TYPE_LONG_BLOB:    // char[]
+      case MYSQL_TYPE_BIT:          // char[]
+      case MYSQL_TYPE_SET:          // char[]
+      case MYSQL_TYPE_ENUM:         // char[]
+      case MYSQL_TYPE_GEOMETRY:     // char[]
+      default:
+        wrapper->result_buffers[i].buffer = xmalloc(fields[i].max_length);
+        wrapper->result_buffers[i].buffer_length = fields[i].max_length;
+        break;
+    }
+
+    wrapper->result_buffers[i].is_null = &wrapper->is_null[i];
+    wrapper->result_buffers[i].length  = &wrapper->length[i];
+    wrapper->result_buffers[i].error   = &wrapper->error[i];
+    wrapper->result_buffers[i].is_unsigned = ((fields[i].flags & UNSIGNED_FLAG) != 0);
+  }
+}
+
+static VALUE rb_mysql_result_fetch_row_stmt(VALUE self, MYSQL_FIELD * fields, const result_each_args *args)
+{
+  VALUE rowVal;
+  unsigned int i = 0;
+
+#ifdef HAVE_RUBY_ENCODING_H
+  rb_encoding *default_internal_enc;
+  rb_encoding *conn_enc;
+#endif
+  GET_RESULT(self);
+
+#ifdef HAVE_RUBY_ENCODING_H
+  default_internal_enc = rb_default_internal_encoding();
+  conn_enc = rb_to_encoding(wrapper->encoding);
+#endif
+
+  if (args->asArray) {
+    rowVal = rb_ary_new2(wrapper->numberOfFields);
+  } else {
+    rowVal = rb_hash_new();
+  }
+  if (wrapper->fields == Qnil) {
+    wrapper->numberOfFields = mysql_num_fields(wrapper->result);
+    wrapper->fields = rb_ary_new2(wrapper->numberOfFields);
+  }
+
+  if (wrapper->result_buffers == NULL) {
+    rb_mysql_result_alloc_result_buffers(self, fields);
+  }
+
+  if (mysql_stmt_bind_result(wrapper->stmt_wrapper->stmt, wrapper->result_buffers)) {
+    rb_raise_mysql2_stmt_error(wrapper->stmt_wrapper);
+  }
+
+  {
+    switch((uintptr_t)rb_thread_call_without_gvl(nogvl_stmt_fetch, wrapper->stmt_wrapper->stmt, RUBY_UBF_IO, 0)) {
+      case 0:
+        /* success */
+        break;
+
+      case 1:
+        /* error */
+        rb_raise_mysql2_stmt_error(wrapper->stmt_wrapper);
+
+      case MYSQL_NO_DATA:
+        /* no more row */
+        return Qnil;
+
+      case MYSQL_DATA_TRUNCATED:
+        rb_raise(cMysql2Error, "IMPLBUG: caught MYSQL_DATA_TRUNCATED. should not come here as buffer_length is set to fields[i].max_length.");
+    }
+  }
+
+  for (i = 0; i < wrapper->numberOfFields; i++) {
+    VALUE field = rb_mysql_result_fetch_field(self, i, args->symbolizeKeys);
+    VALUE val = Qnil;
+    MYSQL_TIME *ts;
+
+    if (wrapper->is_null[i]) {
+      val = Qnil;
+    } else {
+      const MYSQL_BIND* const result_buffer = &wrapper->result_buffers[i];
+
+      switch(result_buffer->buffer_type) {
+        case MYSQL_TYPE_TINY:         // signed char
+          if (args->castBool && fields[i].length == 1) {
+            val = (*((unsigned char*)result_buffer->buffer) != 0) ? Qtrue : Qfalse;
+            break;
+          }
+          if (result_buffer->is_unsigned) {
+            val = UINT2NUM(*((unsigned char*)result_buffer->buffer));
+          } else {
+            val = INT2NUM(*((signed char*)result_buffer->buffer));
+          }
+          break;
+        case MYSQL_TYPE_SHORT:        // short int
+          if (result_buffer->is_unsigned) {
+            val = UINT2NUM(*((unsigned short int*)result_buffer->buffer));
+          } else  {
+            val = INT2NUM(*((short int*)result_buffer->buffer));
+          }
+          break;
+        case MYSQL_TYPE_INT24:        // int
+        case MYSQL_TYPE_LONG:         // int
+        case MYSQL_TYPE_YEAR:         // int
+          if (result_buffer->is_unsigned) {
+            val = UINT2NUM(*((unsigned int*)result_buffer->buffer));
+          } else {
+            val = INT2NUM(*((int*)result_buffer->buffer));
+          }
+          break;
+        case MYSQL_TYPE_LONGLONG:     // long long int
+          if (result_buffer->is_unsigned) {
+            val = ULL2NUM(*((unsigned long long int*)result_buffer->buffer));
+          } else {
+            val = LL2NUM(*((long long int*)result_buffer->buffer));
+          }
+          break;
+        case MYSQL_TYPE_FLOAT:        // float
+          val = rb_float_new((double)(*((float*)result_buffer->buffer)));
+          break;
+        case MYSQL_TYPE_DOUBLE:       // double
+          val = rb_float_new((double)(*((double*)result_buffer->buffer)));
+          break;
+        case MYSQL_TYPE_DATE:         // MYSQL_TIME
+        case MYSQL_TYPE_NEWDATE:      // MYSQL_TIME
+          ts = (MYSQL_TIME*)result_buffer->buffer;
+          val = rb_funcall(cDate, intern_new, 3, INT2NUM(ts->year), INT2NUM(ts->month), INT2NUM(ts->day));
+          break;
+        case MYSQL_TYPE_TIME:         // MYSQL_TIME
+          ts = (MYSQL_TIME*)result_buffer->buffer;
+          val = rb_funcall(rb_cTime, args->db_timezone, 7, opt_time_year, opt_time_month, opt_time_month, UINT2NUM(ts->hour), UINT2NUM(ts->minute), UINT2NUM(ts->second), ULONG2NUM(ts->second_part));
+          if (!NIL_P(args->app_timezone)) {
+            if (args->app_timezone == intern_local) {
+              val = rb_funcall(val, intern_localtime, 0);
+            } else { // utc
+              val = rb_funcall(val, intern_utc, 0);
+            }
+          }
+          break;
+        case MYSQL_TYPE_DATETIME:     // MYSQL_TIME
+        case MYSQL_TYPE_TIMESTAMP: {  // MYSQL_TIME
+          uint64_t seconds;
+
+          ts = (MYSQL_TIME*)result_buffer->buffer;
+          seconds = (ts->year*31557600ULL) + (ts->month*2592000ULL) + (ts->day*86400ULL) + (ts->hour*3600ULL) + (ts->minute*60ULL) + ts->second;
+
+          if (seconds < MYSQL2_MIN_TIME || seconds > MYSQL2_MAX_TIME) { // use DateTime instead
+            VALUE offset = INT2NUM(0);
+            if (args->db_timezone == intern_local) {
+              offset = rb_funcall(cMysql2Client, intern_local_offset, 0);
+            }
+            val = rb_funcall(cDateTime, intern_civil, 7, UINT2NUM(ts->year), UINT2NUM(ts->month), UINT2NUM(ts->day), UINT2NUM(ts->hour), UINT2NUM(ts->minute), UINT2NUM(ts->second), offset);
+            if (!NIL_P(args->app_timezone)) {
+              if (args->app_timezone == intern_local) {
+                offset = rb_funcall(cMysql2Client, intern_local_offset, 0);
+                val = rb_funcall(val, intern_new_offset, 1, offset);
+              } else { // utc
+                val = rb_funcall(val, intern_new_offset, 1, opt_utc_offset);
+              }
+            }
+          } else {
+            val = rb_funcall(rb_cTime, args->db_timezone, 7, UINT2NUM(ts->year), UINT2NUM(ts->month), UINT2NUM(ts->day), UINT2NUM(ts->hour), UINT2NUM(ts->minute), UINT2NUM(ts->second), ULONG2NUM(ts->second_part));
+            if (!NIL_P(args->app_timezone)) {
+              if (args->app_timezone == intern_local) {
+                val = rb_funcall(val, intern_localtime, 0);
+              } else { // utc
+                val = rb_funcall(val, intern_utc, 0);
+              }
+            }
+          }
+          break;
+        }
+        case MYSQL_TYPE_DECIMAL:      // char[]
+        case MYSQL_TYPE_NEWDECIMAL:   // char[]
+          val = rb_funcall(cBigDecimal, intern_new, 1, rb_str_new(result_buffer->buffer, *(result_buffer->length)));
+          break;
+        case MYSQL_TYPE_STRING:       // char[]
+        case MYSQL_TYPE_VAR_STRING:   // char[]
+        case MYSQL_TYPE_VARCHAR:      // char[]
+        case MYSQL_TYPE_TINY_BLOB:    // char[]
+        case MYSQL_TYPE_BLOB:         // char[]
+        case MYSQL_TYPE_MEDIUM_BLOB:  // char[]
+        case MYSQL_TYPE_LONG_BLOB:    // char[]
+        case MYSQL_TYPE_BIT:          // char[]
+        case MYSQL_TYPE_SET:          // char[]
+        case MYSQL_TYPE_ENUM:         // char[]
+        case MYSQL_TYPE_GEOMETRY:     // char[]
+        default:
+          val = rb_str_new(result_buffer->buffer, *(result_buffer->length));
+#ifdef HAVE_RUBY_ENCODING_H
+          val = mysql2_set_field_string_encoding(val, fields[i], default_internal_enc, conn_enc);
+#endif
+          break;
+      }
+    }
+
+    if (args->asArray) {
+      rb_ary_push(rowVal, val);
+    } else {
+      rb_hash_aset(rowVal, field, val);
+    }
+  }
+
+  return rowVal;
+}
+
+
+static VALUE rb_mysql_result_fetch_row(VALUE self, MYSQL_FIELD * fields, const result_each_args *args)
+{
   VALUE rowVal;
-  mysql2_result_wrapper * wrapper;
   MYSQL_ROW row;
   unsigned int i = 0;
   unsigned long * fieldLengths;
@@ -202,7 +523,7 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo
   rb_encoding *default_internal_enc;
   rb_encoding *conn_enc;
 #endif
-  GetMysql2Result(self, wrapper);
+  GET_RESULT(self);
 
 #ifdef HAVE_RUBY_ENCODING_H
   default_internal_enc = rb_default_internal_encoding();
@@ -215,7 +536,7 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo
     return Qnil;
   }
 
-  if (asArray) {
+  if (args->asArray) {
     rowVal = rb_ary_new2(wrapper->numberOfFields);
   } else {
     rowVal = rb_hash_new();
@@ -227,12 +548,12 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo
   }
 
   for (i = 0; i < wrapper->numberOfFields; i++) {
-    VALUE field = rb_mysql_result_fetch_field(self, i, symbolizeKeys);
+    VALUE field = rb_mysql_result_fetch_field(self, i, args->symbolizeKeys);
     if (row[i]) {
       VALUE val = Qnil;
       enum enum_field_types type = fields[i].type;
 
-      if (!cast) {
+      if (!args->cast) {
         if (type == MYSQL_TYPE_NULL) {
           val = Qnil;
         } else {
@@ -247,14 +568,14 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo
           val = Qnil;
           break;
         case MYSQL_TYPE_BIT:        /* BIT field (MySQL 5.0.3 and up) */
-          if (castBool && fields[i].length == 1) {
+          if (args->castBool && fields[i].length == 1) {
             val = *row[i] == 1 ? Qtrue : Qfalse;
           }else{
             val = rb_str_new(row[i], fieldLengths[i]);
           }
           break;
         case MYSQL_TYPE_TINY:       /* TINYINT field */
-          if (castBool && fields[i].length == 1) {
+          if (args->castBool && fields[i].length == 1) {
             val = *row[i] != '0' ? Qtrue : Qfalse;
             break;
           }
@@ -297,9 +618,9 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo
             break;
           }
           msec = msec_char_to_uint(msec_char, sizeof(msec_char));
-          val = rb_funcall(rb_cTime, db_timezone, 7, opt_time_year, opt_time_month, opt_time_month, UINT2NUM(hour), UINT2NUM(min), UINT2NUM(sec), UINT2NUM(msec));
-          if (!NIL_P(app_timezone)) {
-            if (app_timezone == intern_local) {
+          val = rb_funcall(rb_cTime, args->db_timezone, 7, opt_time_year, opt_time_month, opt_time_month, UINT2NUM(hour), UINT2NUM(min), UINT2NUM(sec), UINT2NUM(msec));
+          if (!NIL_P(args->app_timezone)) {
+            if (args->app_timezone == intern_local) {
               val = rb_funcall(val, intern_localtime, 0);
             } else { /* utc */
               val = rb_funcall(val, intern_utc, 0);
@@ -330,12 +651,12 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo
             } else {
               if (seconds < MYSQL2_MIN_TIME || seconds > MYSQL2_MAX_TIME) { /* use DateTime for larger date range, does not support microseconds */
                 VALUE offset = INT2NUM(0);
-                if (db_timezone == intern_local) {
+                if (args->db_timezone == intern_local) {
                   offset = rb_funcall(cMysql2Client, intern_local_offset, 0);
                 }
                 val = rb_funcall(cDateTime, intern_civil, 7, UINT2NUM(year), UINT2NUM(month), UINT2NUM(day), UINT2NUM(hour), UINT2NUM(min), UINT2NUM(sec), offset);
-                if (!NIL_P(app_timezone)) {
-                  if (app_timezone == intern_local) {
+                if (!NIL_P(args->app_timezone)) {
+                  if (args->app_timezone == intern_local) {
                     offset = rb_funcall(cMysql2Client, intern_local_offset, 0);
                     val = rb_funcall(val, intern_new_offset, 1, offset);
                   } else { /* utc */
@@ -344,9 +665,9 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo
                 }
               } else {
                 msec = msec_char_to_uint(msec_char, sizeof(msec_char));
-                val = rb_funcall(rb_cTime, db_timezone, 7, UINT2NUM(year), UINT2NUM(month), UINT2NUM(day), UINT2NUM(hour), UINT2NUM(min), UINT2NUM(sec), UINT2NUM(msec));
-                if (!NIL_P(app_timezone)) {
-                  if (app_timezone == intern_local) {
+                val = rb_funcall(rb_cTime, args->db_timezone, 7, UINT2NUM(year), UINT2NUM(month), UINT2NUM(day), UINT2NUM(hour), UINT2NUM(min), UINT2NUM(sec), UINT2NUM(msec));
+                if (!NIL_P(args->app_timezone)) {
+                  if (args->app_timezone == intern_local) {
                     val = rb_funcall(val, intern_localtime, 0);
                   } else { /* utc */
                     val = rb_funcall(val, intern_utc, 0);
@@ -396,13 +717,13 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo
           break;
         }
       }
-      if (asArray) {
+      if (args->asArray) {
         rb_ary_push(rowVal, val);
       } else {
         rb_hash_aset(rowVal, field, val);
       }
     } else {
-      if (asArray) {
+      if (args->asArray) {
         rb_ary_push(rowVal, Qnil);
       } else {
         rb_hash_aset(rowVal, field, Qnil);
@@ -413,12 +734,11 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo
 }
 
 static VALUE rb_mysql_result_fetch_fields(VALUE self) {
-  mysql2_result_wrapper * wrapper;
   unsigned int i = 0;
   short int symbolizeKeys = 0;
   VALUE defaults;
 
-  GetMysql2Result(self, wrapper);
+  GET_RESULT(self);
 
   defaults = rb_iv_get(self, "@query_options");
   Check_Type(defaults, T_HASH);
@@ -431,7 +751,7 @@ static VALUE rb_mysql_result_fetch_fields(VALUE self) {
     wrapper->fields = rb_ary_new2(wrapper->numberOfFields);
   }
 
-  if (RARRAY_LEN(wrapper->fields) != wrapper->numberOfFields) {
+  if ((my_ulonglong)RARRAY_LEN(wrapper->fields) != wrapper->numberOfFields) {
     for (i=0; i<wrapper->numberOfFields; i++) {
       rb_mysql_result_fetch_field(self, i, symbolizeKeys);
     }
@@ -440,108 +760,38 @@ static VALUE rb_mysql_result_fetch_fields(VALUE self) {
   return wrapper->fields;
 }
 
-static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
-  VALUE defaults, opts, block;
-  ID db_timezone, app_timezone, dbTz, appTz;
-  mysql2_result_wrapper * wrapper;
+static VALUE rb_mysql_result_each_(VALUE self,
+                                   VALUE(*fetch_row_func)(VALUE, MYSQL_FIELD *fields, const result_each_args *args),
+                                   const result_each_args *args)
+{
   unsigned long i;
-  const char * errstr;
-  int symbolizeKeys = 0, asArray = 0, castBool = 0, cacheRows = 1, cast = 1, streaming = 0;
-  MYSQL_FIELD * fields = NULL;
+  const char *errstr;
+  MYSQL_FIELD *fields = NULL;
 
-  GetMysql2Result(self, wrapper);
-
-  defaults = rb_iv_get(self, "@query_options");
-  Check_Type(defaults, T_HASH);
-  if (rb_scan_args(argc, argv, "01&", &opts, &block) == 1) {
-    opts = rb_funcall(defaults, intern_merge, 1, opts);
-  } else {
-    opts = defaults;
-  }
-
-  if (rb_hash_aref(opts, sym_symbolize_keys) == Qtrue) {
-    symbolizeKeys = 1;
-  }
-
-  if (rb_hash_aref(opts, sym_as) == sym_array) {
-    asArray = 1;
-  }
+  GET_RESULT(self);
 
-  if (rb_hash_aref(opts, sym_cast_booleans) == Qtrue) {
-    castBool = 1;
-  }
-
-  if (rb_hash_aref(opts, sym_cache_rows) == Qfalse) {
-    cacheRows = 0;
-  }
-
-  if (rb_hash_aref(opts, sym_cast) == Qfalse) {
-    cast = 0;
-  }
-
-  if (rb_hash_aref(opts, sym_stream) == Qtrue) {
-    streaming = 1;
-  }
-
-  if (streaming && cacheRows) {
-    rb_warn("cacheRows is ignored if streaming is true");
-  }
-
-  dbTz = rb_hash_aref(opts, sym_database_timezone);
-  if (dbTz == sym_local) {
-    db_timezone = intern_local;
-  } else if (dbTz == sym_utc) {
-    db_timezone = intern_utc;
-  } else {
-    if (!NIL_P(dbTz)) {
-      rb_warn(":database_timezone option must be :utc or :local - defaulting to :local");
-    }
-    db_timezone = intern_local;
-  }
-
-  appTz = rb_hash_aref(opts, sym_application_timezone);
-  if (appTz == sym_local) {
-    app_timezone = intern_local;
-  } else if (appTz == sym_utc) {
-    app_timezone = intern_utc;
-  } else {
-    app_timezone = Qnil;
-  }
-
-  if (wrapper->lastRowProcessed == 0) {
-    if (streaming) {
-      /* We can't get number of rows if we're streaming, */
-      /* until we've finished fetching all rows */
-      wrapper->numberOfRows = 0;
+  if (wrapper->is_streaming) {
+    /* When streaming, we will only yield rows, not return them. */
+    if (wrapper->rows == Qnil) {
       wrapper->rows = rb_ary_new();
-    } else {
-      wrapper->numberOfRows = mysql_num_rows(wrapper->result);
-      if (wrapper->numberOfRows == 0) {
-        wrapper->rows = rb_ary_new();
-        return wrapper->rows;
-      }
-      wrapper->rows = rb_ary_new2(wrapper->numberOfRows);
     }
-  }
 
-  if (streaming) {
     if (!wrapper->streamingComplete) {
       VALUE row;
 
       fields = mysql_fetch_fields(wrapper->result);
 
       do {
-        row = rb_mysql_result_fetch_row(self, db_timezone, app_timezone, symbolizeKeys, asArray, castBool, cast, fields);
-
-        if (block != Qnil && row != Qnil) {
-          rb_yield(row);
-          wrapper->lastRowProcessed++;
+        row = fetch_row_func(self, fields, args);
+        if (row != Qnil) {
+          wrapper->numberOfRows++;
+          if (args->block_given != Qnil) {
+            rb_yield(row);
+          }
         }
       } while(row != Qnil);
 
       rb_mysql_result_free_result(wrapper);
-
-      wrapper->numberOfRows = wrapper->lastRowProcessed;
       wrapper->streamingComplete = 1;
 
       // Check for errors, the connection might have gone out from under us
@@ -554,7 +804,7 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
       rb_raise(cMysql2Error, "You have already fetched all the rows for this query and streaming is true. (to reiterate you must requery).");
     }
   } else {
-    if (cacheRows && wrapper->lastRowProcessed == wrapper->numberOfRows) {
+    if (args->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++) {
@@ -567,11 +817,11 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
 
       for (i = 0; i < wrapper->numberOfRows; i++) {
         VALUE row;
-        if (cacheRows && i < rowsProcessed) {
+        if (args->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, cast, fields);
-          if (cacheRows) {
+          row = fetch_row_func(self, fields, args);
+          if (args->cacheRows) {
             rb_ary_store(wrapper->rows, i, row);
           }
           wrapper->lastRowProcessed++;
@@ -583,7 +833,7 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
           return Qnil;
         }
 
-        if (block != Qnil) {
+        if (args->block_given != Qnil) {
           rb_yield(row);
         }
       }
@@ -594,28 +844,124 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
     }
   }
 
+  // FIXME return Enumerator instead?
+  // return rb_ary_each(wrapper->rows);
   return wrapper->rows;
 }
 
+static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
+  result_each_args args;
+  VALUE defaults, opts, block, (*fetch_row_func)(VALUE, MYSQL_FIELD *fields, const result_each_args *args);
+  ID db_timezone, app_timezone, dbTz, appTz;
+  int symbolizeKeys, asArray, castBool, cacheRows, cast;
+
+  GET_RESULT(self);
+
+  if (wrapper->stmt_wrapper && wrapper->stmt_wrapper->closed) {
+    rb_raise(cMysql2Error, "Statement handle already closed");
+  }
+
+  defaults = rb_iv_get(self, "@query_options");
+  Check_Type(defaults, T_HASH);
+  if (rb_scan_args(argc, argv, "01&", &opts, &block) == 1) {
+    opts = rb_funcall(defaults, intern_merge, 1, opts);
+  } else {
+    opts = defaults;
+  }
+
+  symbolizeKeys = RTEST(rb_hash_aref(opts, sym_symbolize_keys));
+  asArray       = rb_hash_aref(opts, sym_as) == sym_array;
+  castBool      = RTEST(rb_hash_aref(opts, sym_cast_booleans));
+  cacheRows     = RTEST(rb_hash_aref(opts, sym_cache_rows));
+  cast          = RTEST(rb_hash_aref(opts, sym_cast));
+
+  if (wrapper->is_streaming && cacheRows) {
+    rb_warn(":cache_rows is ignored if :stream is true");
+  }
+
+  if (wrapper->stmt_wrapper && !cacheRows && !wrapper->is_streaming) {
+    rb_warn(":cache_rows is forced for prepared statements (if not streaming)");
+  }
+
+  if (wrapper->stmt_wrapper && !cast) {
+    rb_warn(":cast is forced for prepared statements");
+  }
+
+  dbTz = rb_hash_aref(opts, sym_database_timezone);
+  if (dbTz == sym_local) {
+    db_timezone = intern_local;
+  } else if (dbTz == sym_utc) {
+    db_timezone = intern_utc;
+  } else {
+    if (!NIL_P(dbTz)) {
+      rb_warn(":database_timezone option must be :utc or :local - defaulting to :local");
+    }
+    db_timezone = intern_local;
+  }
+
+  appTz = rb_hash_aref(opts, sym_application_timezone);
+  if (appTz == sym_local) {
+    app_timezone = intern_local;
+  } else if (appTz == sym_utc) {
+    app_timezone = intern_utc;
+  } else {
+    app_timezone = Qnil;
+  }
+
+  if (wrapper->lastRowProcessed == 0 && !wrapper->is_streaming) {
+    wrapper->numberOfRows = wrapper->stmt_wrapper ? mysql_stmt_num_rows(wrapper->stmt_wrapper->stmt) : mysql_num_rows(wrapper->result);
+    if (wrapper->numberOfRows == 0) {
+      wrapper->rows = rb_ary_new();
+      return wrapper->rows;
+    }
+    wrapper->rows = rb_ary_new2(wrapper->numberOfRows);
+  }
+
+  // Backward compat
+  args.symbolizeKeys = symbolizeKeys;
+  args.asArray = asArray;
+  args.castBool = castBool;
+  args.cacheRows = cacheRows;
+  args.cast = cast;
+  args.db_timezone = db_timezone;
+  args.app_timezone = app_timezone;
+  args.block_given = block;
+
+  if (wrapper->stmt_wrapper) {
+    fetch_row_func = rb_mysql_result_fetch_row_stmt;
+  } else {
+    fetch_row_func = rb_mysql_result_fetch_row;
+  }
+
+  return rb_mysql_result_each_(self, fetch_row_func, &args);
+}
+
 static VALUE rb_mysql_result_count(VALUE self) {
-  mysql2_result_wrapper *wrapper;
+  GET_RESULT(self);
+
+  if (wrapper->is_streaming) {
+    /* This is an unsigned long per result.h */
+    return ULONG2NUM(wrapper->numberOfRows);
+  }
 
-  GetMysql2Result(self, wrapper);
   if (wrapper->resultFreed) {
-    if (wrapper->streamingComplete){
-      return LONG2NUM(wrapper->numberOfRows);
+    /* Ruby arrays have platform signed long length */
+    return LONG2NUM(RARRAY_LEN(wrapper->rows));
+  } else {
+    /* MySQL returns an unsigned 64-bit long here */
+    if (wrapper->stmt_wrapper) {
+      return ULL2NUM(mysql_stmt_num_rows(wrapper->stmt_wrapper->stmt));
     } else {
-      return LONG2NUM(RARRAY_LEN(wrapper->rows));
+      return ULL2NUM(mysql_num_rows(wrapper->result));
     }
-  } else {
-    return INT2FIX(mysql_num_rows(wrapper->result));
   }
 }
 
 /* Mysql2::Result */
-VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_RES *r) {
+VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_RES *r, VALUE statement) {
   VALUE obj;
   mysql2_result_wrapper * wrapper;
+
   obj = Data_Make_Struct(cMysql2Result, mysql2_result_wrapper, rb_mysql_result_mark, rb_mysql_result_free, wrapper);
   wrapper->numberOfFields = 0;
   wrapper->numberOfRows = 0;
@@ -629,11 +975,27 @@ VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_
   wrapper->client = client;
   wrapper->client_wrapper = DATA_PTR(client);
   wrapper->client_wrapper->refcount++;
+  wrapper->result_buffers = NULL;
+  wrapper->is_null = NULL;
+  wrapper->error = NULL;
+  wrapper->length = NULL;
+
+  /* Keep a handle to the Statement to ensure it doesn't get garbage collected first */
+  wrapper->statement = statement;
+  if (statement != Qnil) {
+    wrapper->stmt_wrapper = DATA_PTR(statement);
+    wrapper->stmt_wrapper->refcount++;
+  } else {
+    wrapper->stmt_wrapper = NULL;
+  }
 
   rb_obj_call_init(obj, 0, NULL);
-
   rb_iv_set(obj, "@query_options", options);
 
+  /* Options that cannot be changed in results.each(...) { |row| }
+   * should be processed here. */
+  wrapper->is_streaming = (rb_hash_aref(options, sym_stream) == Qtrue ? 1 : 0);
+
   return obj;
 }
 
diff --git a/ext/mysql2/result.h b/ext/mysql2/result.h
index 2bb6207..0c25b24 100644
--- a/ext/mysql2/result.h
+++ b/ext/mysql2/result.h
@@ -1,23 +1,29 @@
 #ifndef MYSQL2_RESULT_H
 #define MYSQL2_RESULT_H
 
-void init_mysql2_result();
-VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_RES *r);
+void init_mysql2_result(void);
+VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_RES *r, VALUE statement);
 
 typedef struct {
   VALUE fields;
   VALUE rows;
   VALUE client;
   VALUE encoding;
-  unsigned int numberOfFields;
-  unsigned long numberOfRows;
+  VALUE statement;
+  my_ulonglong numberOfFields;
+  my_ulonglong numberOfRows;
   unsigned long lastRowProcessed;
+  char is_streaming;
   char streamingComplete;
   char resultFreed;
   MYSQL_RES *result;
+  mysql_stmt_wrapper *stmt_wrapper;
   mysql_client_wrapper *client_wrapper;
+  /* statement result bind buffers */
+  MYSQL_BIND *result_buffers;
+  my_bool *is_null;
+  my_bool *error;
+  unsigned long *length;
 } mysql2_result_wrapper;
 
-#define GetMysql2Result(obj, sval) (sval = (mysql2_result_wrapper*)DATA_PTR(obj));
-
 #endif
diff --git a/ext/mysql2/statement.c b/ext/mysql2/statement.c
new file mode 100644
index 0000000..fc61e93
--- /dev/null
+++ b/ext/mysql2/statement.c
@@ -0,0 +1,494 @@
+#include <mysql2_ext.h>
+
+VALUE cMysql2Statement;
+extern VALUE mMysql2, cMysql2Error, cBigDecimal, cDateTime, cDate;
+static VALUE sym_stream, intern_new_with_args, intern_each;
+static VALUE intern_usec, intern_sec, intern_min, intern_hour, intern_day, intern_month, intern_year;
+
+#define GET_STATEMENT(self) \
+  mysql_stmt_wrapper *stmt_wrapper; \
+  Data_Get_Struct(self, mysql_stmt_wrapper, stmt_wrapper); \
+  if (!stmt_wrapper->stmt) { rb_raise(cMysql2Error, "Invalid statement handle"); } \
+  if (stmt_wrapper->closed) { rb_raise(cMysql2Error, "Statement handle already closed"); }
+
+
+static void rb_mysql_stmt_mark(void * ptr) {
+  mysql_stmt_wrapper *stmt_wrapper = ptr;
+  if (!stmt_wrapper) return;
+
+  rb_gc_mark(stmt_wrapper->client);
+}
+
+static void *nogvl_stmt_close(void * ptr) {
+  mysql_stmt_wrapper *stmt_wrapper = ptr;
+  if (stmt_wrapper->stmt) {
+    mysql_stmt_close(stmt_wrapper->stmt);
+    stmt_wrapper->stmt = NULL;
+  }
+  return NULL;
+}
+
+static void rb_mysql_stmt_free(void * ptr) {
+  mysql_stmt_wrapper *stmt_wrapper = ptr;
+  decr_mysql2_stmt(stmt_wrapper);
+}
+
+void decr_mysql2_stmt(mysql_stmt_wrapper *stmt_wrapper) {
+  stmt_wrapper->refcount--;
+
+  if (stmt_wrapper->refcount == 0) {
+    nogvl_stmt_close(stmt_wrapper);
+    xfree(stmt_wrapper);
+  }
+}
+
+
+void rb_raise_mysql2_stmt_error(mysql_stmt_wrapper *stmt_wrapper) {
+  VALUE e;
+  GET_CLIENT(stmt_wrapper->client);
+  VALUE rb_error_msg = rb_str_new2(mysql_stmt_error(stmt_wrapper->stmt));
+  VALUE rb_sql_state = rb_tainted_str_new2(mysql_stmt_sqlstate(stmt_wrapper->stmt));
+
+#ifdef HAVE_RUBY_ENCODING_H
+  rb_encoding *conn_enc;
+  conn_enc = rb_to_encoding(wrapper->encoding);
+
+  rb_encoding *default_internal_enc = rb_default_internal_encoding();
+
+  rb_enc_associate(rb_error_msg, conn_enc);
+  rb_enc_associate(rb_sql_state, conn_enc);
+  if (default_internal_enc) {
+    rb_error_msg = rb_str_export_to_enc(rb_error_msg, default_internal_enc);
+    rb_sql_state = rb_str_export_to_enc(rb_sql_state, default_internal_enc);
+  }
+#endif
+
+  e = rb_funcall(cMysql2Error, intern_new_with_args, 4,
+                 rb_error_msg,
+                 LONG2FIX(wrapper->server_version),
+                 UINT2NUM(mysql_stmt_errno(stmt_wrapper->stmt)),
+                 rb_sql_state);
+  rb_exc_raise(e);
+}
+
+
+/*
+ * used to pass all arguments to mysql_stmt_prepare while inside
+ * nogvl_prepare_statement_args
+ */
+struct nogvl_prepare_statement_args {
+  MYSQL_STMT *stmt;
+  VALUE sql;
+  const char *sql_ptr;
+  unsigned long sql_len;
+};
+
+static void *nogvl_prepare_statement(void *ptr) {
+  struct nogvl_prepare_statement_args *args = ptr;
+
+  if (mysql_stmt_prepare(args->stmt, args->sql_ptr, args->sql_len)) {
+    return (void*)Qfalse;
+  } else {
+    return (void*)Qtrue;
+  }
+}
+
+VALUE rb_mysql_stmt_new(VALUE rb_client, VALUE sql) {
+  mysql_stmt_wrapper *stmt_wrapper;
+  VALUE rb_stmt;
+#ifdef HAVE_RUBY_ENCODING_H
+  rb_encoding *conn_enc;
+#endif
+
+  Check_Type(sql, T_STRING);
+
+  rb_stmt = Data_Make_Struct(cMysql2Statement, mysql_stmt_wrapper, rb_mysql_stmt_mark, rb_mysql_stmt_free, stmt_wrapper);
+  {
+    stmt_wrapper->client = rb_client;
+    stmt_wrapper->refcount = 1;
+    stmt_wrapper->closed = 0;
+    stmt_wrapper->stmt = NULL;
+  }
+
+  // instantiate stmt
+  {
+    GET_CLIENT(rb_client);
+    stmt_wrapper->stmt = mysql_stmt_init(wrapper->client);
+#ifdef HAVE_RUBY_ENCODING_H
+    conn_enc = rb_to_encoding(wrapper->encoding);
+#endif
+  }
+  if (stmt_wrapper->stmt == NULL) {
+    rb_raise(cMysql2Error, "Unable to initialize prepared statement: out of memory");
+  }
+
+  // set STMT_ATTR_UPDATE_MAX_LENGTH attr
+  {
+    my_bool truth = 1;
+    if (mysql_stmt_attr_set(stmt_wrapper->stmt, STMT_ATTR_UPDATE_MAX_LENGTH, &truth)) {
+      rb_raise(cMysql2Error, "Unable to initialize prepared statement: set STMT_ATTR_UPDATE_MAX_LENGTH");
+    }
+  }
+
+  // call mysql_stmt_prepare w/o gvl
+  {
+    struct nogvl_prepare_statement_args args;
+    args.stmt = stmt_wrapper->stmt;
+    args.sql = sql;
+#ifdef HAVE_RUBY_ENCODING_H
+    // ensure the string is in the encoding the connection is expecting
+    args.sql = rb_str_export_to_enc(args.sql, conn_enc);
+#endif
+    args.sql_ptr = RSTRING_PTR(sql);
+    args.sql_len = RSTRING_LEN(sql);
+
+    if ((VALUE)rb_thread_call_without_gvl(nogvl_prepare_statement, &args, RUBY_UBF_IO, 0) == Qfalse) {
+      rb_raise_mysql2_stmt_error(stmt_wrapper);
+    }
+  }
+
+  return rb_stmt;
+}
+
+/* call-seq: stmt.param_count # => Numeric
+ *
+ * Returns the number of parameters the prepared statement expects.
+ */
+static VALUE param_count(VALUE self) {
+  GET_STATEMENT(self);
+
+  return ULL2NUM(mysql_stmt_param_count(stmt_wrapper->stmt));
+}
+
+/* call-seq: stmt.field_count # => Numeric
+ *
+ * Returns the number of fields the prepared statement returns.
+ */
+static VALUE field_count(VALUE self) {
+  GET_STATEMENT(self);
+
+  return UINT2NUM(mysql_stmt_field_count(stmt_wrapper->stmt));
+}
+
+static void *nogvl_execute(void *ptr) {
+  MYSQL_STMT *stmt = ptr;
+
+  if (mysql_stmt_execute(stmt)) {
+    return (void*)Qfalse;
+  } else {
+    return (void*)Qtrue;
+  }
+}
+
+static void *nogvl_stmt_store_result(void *ptr) {
+  MYSQL_STMT *stmt = ptr;
+
+  if (mysql_stmt_store_result(stmt)) {
+    return (void *)Qfalse;
+  } else {
+    return (void *)Qtrue;
+  }
+}
+
+/* Free each bind_buffer[i].buffer except when params_enc is non-nil, this means
+ * the buffer is a Ruby string pointer and not our memory to manage.
+ */
+#define FREE_BINDS                                          \
+  for (i = 0; i < argc; i++) {                              \
+    if (bind_buffers[i].buffer && NIL_P(params_enc[i])) {   \
+      xfree(bind_buffers[i].buffer);                        \
+    }                                                       \
+  }                                                         \
+  if (argc > 0) {                                           \
+    xfree(bind_buffers);                                    \
+    xfree(length_buffers);                                  \
+  }
+
+/* call-seq: stmt.execute
+ *
+ * Executes the current prepared statement, returns +result+.
+ */
+static VALUE execute(int argc, VALUE *argv, VALUE self) {
+  MYSQL_BIND *bind_buffers = NULL;
+  unsigned long *length_buffers = NULL;
+  unsigned long bind_count;
+  long i;
+  MYSQL_STMT *stmt;
+  MYSQL_RES *metadata;
+  VALUE current;
+  VALUE resultObj;
+  VALUE *params_enc;
+  int is_streaming;
+#ifdef HAVE_RUBY_ENCODING_H
+  rb_encoding *conn_enc;
+#endif
+
+  GET_STATEMENT(self);
+  GET_CLIENT(stmt_wrapper->client);
+
+#ifdef HAVE_RUBY_ENCODING_H
+  conn_enc = rb_to_encoding(wrapper->encoding);
+#endif
+
+  /* Scratch space for string encoding exports, allocate on the stack. */
+  params_enc = alloca(sizeof(VALUE) * argc);
+
+  stmt = stmt_wrapper->stmt;
+
+  bind_count = mysql_stmt_param_count(stmt);
+  if (argc != (long)bind_count) {
+    rb_raise(cMysql2Error, "Bind parameter count (%ld) doesn't match number of arguments (%d)", bind_count, argc);
+  }
+
+  // setup any bind variables in the query
+  if (bind_count > 0) {
+    bind_buffers = xcalloc(bind_count, sizeof(MYSQL_BIND));
+    length_buffers = xcalloc(bind_count, sizeof(unsigned long));
+
+    for (i = 0; i < argc; i++) {
+      bind_buffers[i].buffer = NULL;
+      params_enc[i] = Qnil;
+
+      switch (TYPE(argv[i])) {
+        case T_NIL:
+          bind_buffers[i].buffer_type = MYSQL_TYPE_NULL;
+          break;
+        case T_FIXNUM:
+#if SIZEOF_INT < SIZEOF_LONG
+          bind_buffers[i].buffer_type = MYSQL_TYPE_LONGLONG;
+          bind_buffers[i].buffer = xmalloc(sizeof(long long int));
+          *(long*)(bind_buffers[i].buffer) = FIX2LONG(argv[i]);
+#else
+          bind_buffers[i].buffer_type = MYSQL_TYPE_LONG;
+          bind_buffers[i].buffer = xmalloc(sizeof(int));
+          *(long*)(bind_buffers[i].buffer) = FIX2INT(argv[i]);
+#endif
+          break;
+        case T_BIGNUM:
+          bind_buffers[i].buffer_type = MYSQL_TYPE_LONGLONG;
+          bind_buffers[i].buffer = xmalloc(sizeof(long long int));
+          *(LONG_LONG*)(bind_buffers[i].buffer) = rb_big2ll(argv[i]);
+          break;
+        case T_FLOAT:
+          bind_buffers[i].buffer_type = MYSQL_TYPE_DOUBLE;
+          bind_buffers[i].buffer = xmalloc(sizeof(double));
+          *(double*)(bind_buffers[i].buffer) = NUM2DBL(argv[i]);
+          break;
+        case T_STRING:
+          {
+            params_enc[i] = argv[i];
+#ifdef HAVE_RUBY_ENCODING_H
+            params_enc[i] = rb_str_export_to_enc(params_enc[i], conn_enc);
+#endif
+            bind_buffers[i].buffer_type = MYSQL_TYPE_STRING;
+            bind_buffers[i].buffer = RSTRING_PTR(params_enc[i]);
+            bind_buffers[i].buffer_length = RSTRING_LEN(params_enc[i]);
+            length_buffers[i] = bind_buffers[i].buffer_length;
+            bind_buffers[i].length = &length_buffers[i];
+          }
+          break;
+        default:
+          // TODO: what Ruby type should support MYSQL_TYPE_TIME
+          if (CLASS_OF(argv[i]) == rb_cTime || CLASS_OF(argv[i]) == cDateTime) {
+            MYSQL_TIME t;
+            VALUE rb_time = argv[i];
+
+            bind_buffers[i].buffer_type = MYSQL_TYPE_DATETIME;
+            bind_buffers[i].buffer = xmalloc(sizeof(MYSQL_TIME));
+
+            memset(&t, 0, sizeof(MYSQL_TIME));
+            t.neg = 0;
+            t.second_part = FIX2INT(rb_funcall(rb_time, intern_usec, 0));
+            t.second = FIX2INT(rb_funcall(rb_time, intern_sec, 0));
+            t.minute = FIX2INT(rb_funcall(rb_time, intern_min, 0));
+            t.hour = FIX2INT(rb_funcall(rb_time, intern_hour, 0));
+            t.day = FIX2INT(rb_funcall(rb_time, intern_day, 0));
+            t.month = FIX2INT(rb_funcall(rb_time, intern_month, 0));
+            t.year = FIX2INT(rb_funcall(rb_time, intern_year, 0));
+
+            *(MYSQL_TIME*)(bind_buffers[i].buffer) = t;
+          } else if (CLASS_OF(argv[i]) == cDate) {
+            MYSQL_TIME t;
+            VALUE rb_time = argv[i];
+
+            bind_buffers[i].buffer_type = MYSQL_TYPE_DATE;
+            bind_buffers[i].buffer = xmalloc(sizeof(MYSQL_TIME));
+
+            memset(&t, 0, sizeof(MYSQL_TIME));
+            t.second_part = 0;
+            t.neg = 0;
+            t.day = FIX2INT(rb_funcall(rb_time, intern_day, 0));
+            t.month = FIX2INT(rb_funcall(rb_time, intern_month, 0));
+            t.year = FIX2INT(rb_funcall(rb_time, intern_year, 0));
+
+            *(MYSQL_TIME*)(bind_buffers[i].buffer) = t;
+          } else if (CLASS_OF(argv[i]) == cBigDecimal) {
+            bind_buffers[i].buffer_type = MYSQL_TYPE_NEWDECIMAL;
+          }
+          break;
+      }
+    }
+
+    // copies bind_buffers into internal storage
+    if (mysql_stmt_bind_param(stmt, bind_buffers)) {
+      FREE_BINDS;
+      rb_raise_mysql2_stmt_error(stmt_wrapper);
+    }
+  }
+
+  if ((VALUE)rb_thread_call_without_gvl(nogvl_execute, stmt, RUBY_UBF_IO, 0) == Qfalse) {
+    FREE_BINDS;
+    rb_raise_mysql2_stmt_error(stmt_wrapper);
+  }
+
+  FREE_BINDS;
+
+  metadata = mysql_stmt_result_metadata(stmt);
+  if (metadata == NULL) {
+    if (mysql_stmt_errno(stmt) != 0) {
+      // either CR_OUT_OF_MEMORY or CR_UNKNOWN_ERROR. both fatal.
+
+      MARK_CONN_INACTIVE(stmt_wrapper->client);
+      rb_raise_mysql2_stmt_error(stmt_wrapper);
+    }
+    // no data and no error, so query was not a SELECT
+    return Qnil;
+  }
+
+  current = rb_hash_dup(rb_iv_get(stmt_wrapper->client, "@query_options"));
+  (void)RB_GC_GUARD(current);
+  Check_Type(current, T_HASH);
+
+  is_streaming = (Qtrue == rb_hash_aref(current, sym_stream));
+  if (!is_streaming) {
+    // recieve the whole result set from the server
+    if (rb_thread_call_without_gvl(nogvl_stmt_store_result, stmt, RUBY_UBF_IO, 0) == Qfalse) {
+      mysql_free_result(metadata);
+      rb_raise_mysql2_stmt_error(stmt_wrapper);
+    }
+    MARK_CONN_INACTIVE(stmt_wrapper->client);
+  }
+
+  resultObj = rb_mysql_result_to_obj(stmt_wrapper->client, wrapper->encoding, current, metadata, self);
+
+  if (!is_streaming) {
+    // cache all result
+    rb_funcall(resultObj, intern_each, 0);
+  }
+
+  return resultObj;
+}
+
+/* call-seq: stmt.fields # => array
+ *
+ * Returns a list of fields that will be returned by this statement.
+ */
+static VALUE fields(VALUE self) {
+  MYSQL_FIELD *fields;
+  MYSQL_RES *metadata;
+  unsigned int field_count;
+  unsigned int i;
+  VALUE field_list;
+  MYSQL_STMT* stmt;
+#ifdef HAVE_RUBY_ENCODING_H
+  rb_encoding *default_internal_enc, *conn_enc;
+#endif
+  GET_STATEMENT(self);
+  stmt = stmt_wrapper->stmt;
+
+#ifdef HAVE_RUBY_ENCODING_H
+  default_internal_enc = rb_default_internal_encoding();
+  {
+    GET_CLIENT(stmt_wrapper->client);
+    conn_enc = rb_to_encoding(wrapper->encoding);
+  }
+#endif
+
+  metadata    = mysql_stmt_result_metadata(stmt);
+  fields      = mysql_fetch_fields(metadata);
+  field_count = mysql_stmt_field_count(stmt);
+  field_list  = rb_ary_new2((long)field_count);
+
+  for(i = 0; i < field_count; i++) {
+    VALUE rb_field;
+
+    rb_field = rb_str_new(fields[i].name, fields[i].name_length);
+#ifdef HAVE_RUBY_ENCODING_H
+    rb_enc_associate(rb_field, conn_enc);
+    if (default_internal_enc) {
+     rb_field = rb_str_export_to_enc(rb_field, default_internal_enc);
+   }
+#endif
+
+    rb_ary_store(field_list, (long)i, rb_field);
+  }
+
+  mysql_free_result(metadata);
+  return field_list;
+}
+
+/* call-seq:
+ *    stmt.last_id
+ *
+ * Returns the AUTO_INCREMENT value from the executed INSERT or UPDATE.
+ */
+static VALUE rb_mysql_stmt_last_id(VALUE self) {
+  GET_STATEMENT(self);
+  return ULL2NUM(mysql_stmt_insert_id(stmt_wrapper->stmt));
+}
+
+/* call-seq:
+ *    stmt.affected_rows
+ *
+ * Returns the number of rows changed, deleted, or inserted.
+ */
+static VALUE rb_mysql_stmt_affected_rows(VALUE self) {
+  my_ulonglong affected;
+  GET_STATEMENT(self);
+
+  affected = mysql_stmt_affected_rows(stmt_wrapper->stmt);
+  if (affected == (my_ulonglong)-1) {
+    rb_raise_mysql2_stmt_error(stmt_wrapper);
+  }
+
+  return ULL2NUM(affected);
+}
+
+/* call-seq:
+ *    stmt.close
+ *
+ * Explicitly closing this will free up server resources immediately rather
+ * than waiting for the garbage collector. Useful if you're managing your
+ * own prepared statement cache.
+ */
+static VALUE rb_mysql_stmt_close(VALUE self) {
+  GET_STATEMENT(self);
+  stmt_wrapper->closed = 1;
+  rb_thread_call_without_gvl(nogvl_stmt_close, stmt_wrapper, RUBY_UBF_IO, 0);
+  return Qnil;
+}
+
+void init_mysql2_statement() {
+  cMysql2Statement = rb_define_class_under(mMysql2, "Statement", rb_cObject);
+
+  rb_define_method(cMysql2Statement, "param_count", param_count, 0);
+  rb_define_method(cMysql2Statement, "field_count", field_count, 0);
+  rb_define_method(cMysql2Statement, "_execute", execute, -1);
+  rb_define_method(cMysql2Statement, "fields", fields, 0);
+  rb_define_method(cMysql2Statement, "last_id", rb_mysql_stmt_last_id, 0);
+  rb_define_method(cMysql2Statement, "affected_rows", rb_mysql_stmt_affected_rows, 0);
+  rb_define_method(cMysql2Statement, "close", rb_mysql_stmt_close, 0);
+
+  sym_stream = ID2SYM(rb_intern("stream"));
+
+  intern_new_with_args = rb_intern("new_with_args");
+  intern_each = rb_intern("each");
+
+  intern_usec = rb_intern("usec");
+  intern_sec = rb_intern("sec");
+  intern_min = rb_intern("min");
+  intern_hour = rb_intern("hour");
+  intern_day = rb_intern("day");
+  intern_month = rb_intern("month");
+  intern_year = rb_intern("year");
+}
diff --git a/ext/mysql2/statement.h b/ext/mysql2/statement.h
new file mode 100644
index 0000000..63260aa
--- /dev/null
+++ b/ext/mysql2/statement.h
@@ -0,0 +1,19 @@
+#ifndef MYSQL2_STATEMENT_H
+#define MYSQL2_STATEMENT_H
+
+extern VALUE cMysql2Statement;
+
+typedef struct {
+  VALUE client;
+  MYSQL_STMT *stmt;
+  int refcount;
+  int closed;
+} mysql_stmt_wrapper;
+
+void init_mysql2_statement(void);
+void decr_mysql2_stmt(mysql_stmt_wrapper *stmt_wrapper);
+
+VALUE rb_mysql_stmt_new(VALUE rb_client, VALUE sql);
+void rb_raise_mysql2_stmt_error(mysql_stmt_wrapper *stmt_wrapper) RB_MYSQL_NORETURN;
+
+#endif
diff --git a/lib/mysql2.rb b/lib/mysql2.rb
index f4ae8b1..a45cace 100644
--- a/lib/mysql2.rb
+++ b/lib/mysql2.rb
@@ -8,16 +8,16 @@ require 'rational' unless RUBY_VERSION >= '1.9.2'
 # Or to bomb out with a clear error message instead of a linker crash
 if RUBY_PLATFORM =~ /mswin|mingw/
   dll_path = if ENV['RUBY_MYSQL2_LIBMYSQL_DLL']
-               # If this environment variable is set, it overrides any other paths
-               # The user is advised to use backslashes not forward slashes
-               ENV['RUBY_MYSQL2_LIBMYSQL_DLL'].dup
-             elsif File.exist?(File.expand_path('../vendor/libmysql.dll', File.dirname(__FILE__)))
-               # Use vendor/libmysql.dll if it exists, convert slashes for Win32 LoadLibrary
-               File.expand_path('../vendor/libmysql.dll', File.dirname(__FILE__)).gsub('/', '\\')
-             else
-               # This will use default / system library paths
-               'libmysql.dll'
-             end
+    # If this environment variable is set, it overrides any other paths
+    # The user is advised to use backslashes not forward slashes
+    ENV['RUBY_MYSQL2_LIBMYSQL_DLL']
+  elsif File.exist?(File.expand_path('../vendor/libmysql.dll', File.dirname(__FILE__)))
+    # Use vendor/libmysql.dll if it exists, convert slashes for Win32 LoadLibrary
+    File.expand_path('../vendor/libmysql.dll', File.dirname(__FILE__)).tr('/', '\\')
+  else
+    # This will use default / system library paths
+    'libmysql.dll'
+  end
 
   require 'Win32API'
   LoadLibrary = Win32API.new('Kernel32', 'LoadLibrary', ['P'], 'I')
@@ -31,6 +31,8 @@ require 'mysql2/error'
 require 'mysql2/mysql2'
 require 'mysql2/result'
 require 'mysql2/client'
+require 'mysql2/field'
+require 'mysql2/statement'
 
 # = Mysql2
 #
@@ -51,14 +53,32 @@ if defined?(ActiveRecord::VERSION::STRING) && ActiveRecord::VERSION::STRING < "3
 end
 
 # For holding utility methods
-module Mysql2::Util
+module Mysql2
+  module Util
+    #
+    # Rekey a string-keyed hash with equivalent symbols.
+    #
+    def self.key_hash_as_symbols(hash)
+      return nil unless hash
+      Hash[hash.map { |k, v| [k.to_sym, v] }]
+    end
 
-  #
-  # Rekey a string-keyed hash with equivalent symbols.
-  #
-  def self.key_hash_as_symbols(hash)
-    return nil unless hash
-    Hash[hash.map { |k,v| [k.to_sym, v] }]
+    #
+    # In Mysql2::Client#query and Mysql2::Statement#execute,
+    # Thread#handle_interrupt is used to prevent Timeout#timeout
+    # from interrupting query execution.
+    #
+    # Timeout::ExitException was removed in Ruby 2.3.0, 2.2.3, and 2.1.8,
+    # but is present in earlier 2.1.x and 2.2.x, so we provide a shim.
+    #
+    if Thread.respond_to?(:handle_interrupt)
+      require 'timeout'
+      # rubocop:disable Style/ConstantName
+      TimeoutError = if defined?(::Timeout::ExitException)
+        ::Timeout::ExitException
+      else
+        ::Timeout::Error
+      end
+    end
   end
-
 end
diff --git a/lib/mysql2/client.rb b/lib/mysql2/client.rb
index 99a043f..7178f2c 100644
--- a/lib/mysql2/client.rb
+++ b/lib/mysql2/client.rb
@@ -1,24 +1,27 @@
 module Mysql2
   class Client
     attr_reader :query_options, :read_timeout
-    @@default_query_options = {
-      :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
-      :connect_flags => REMEMBER_OPTIONS | LONG_PASSWORD | LONG_FLAG | TRANSACTIONS | PROTOCOL_41 | SECURE_CONNECTION,
-      :cast => true,
-      :default_file => nil,
-      :default_group => nil
-    }
+
+    def self.default_query_options
+      @default_query_options ||= {
+        :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 its internal row cache for results
+        :connect_flags => REMEMBER_OPTIONS | LONG_PASSWORD | LONG_FLAG | TRANSACTIONS | PROTOCOL_41 | SECURE_CONNECTION,
+        :cast => true,
+        :default_file => nil,
+        :default_group => nil,
+      }
+    end
 
     def initialize(opts = {})
-      opts = Mysql2::Util.key_hash_as_symbols( opts )
+      opts = Mysql2::Util.key_hash_as_symbols(opts)
       @read_timeout = nil
-      @query_options = @@default_query_options.dup
+      @query_options = self.class.default_query_options.dup
       @query_options.merge! opts
 
       initialize_ext
@@ -26,11 +29,12 @@ module Mysql2
       # Set default connect_timeout to avoid unlimited retries from signal interruption
       opts[:connect_timeout] = 120 unless opts.key?(:connect_timeout)
 
+      # TODO: stricter validation rather than silent massaging
       [:reconnect, :connect_timeout, :local_infile, :read_timeout, :write_timeout, :default_file, :default_group, :secure_auth, :init_command].each do |key|
         next unless opts.key?(key)
         case key
         when :reconnect, :local_infile, :secure_auth
-          send(:"#{key}=", !!opts[key])
+          send(:"#{key}=", !!opts[key]) # rubocop:disable Style/DoubleNegation
         when :connect_timeout, :read_timeout, :write_timeout
           send(:"#{key}=", opts[key].to_i)
         else
@@ -44,7 +48,21 @@ module Mysql2
       ssl_options = opts.values_at(:sslkey, :sslcert, :sslca, :sslcapath, :sslcipher)
       ssl_set(*ssl_options) if ssl_options.any?
 
-      if [:user,:pass,:hostname,:dbname,:db,:sock].any?{|k| @query_options.has_key?(k) }
+      case opts[:flags]
+      when Array
+        flags = parse_flags_array(opts[:flags], @query_options[:connect_flags])
+      when String
+        flags = parse_flags_array(opts[:flags].split(' '), @query_options[:connect_flags])
+      when Integer
+        flags = @query_options[:connect_flags] | opts[:flags]
+      else
+        flags = @query_options[:connect_flags]
+      end
+
+      # SSL verify is a connection flag rather than a mysql_ssl_set option
+      flags |= SSL_VERIFY_SERVER_CERT if opts[:sslverify] && ssl_options.any?
+
+      if [:user, :pass, :hostname, :dbname, :db, :sock].any? { |k| @query_options.key?(k) }
         warn "============= WARNING FROM mysql2 ============="
         warn "The options :user, :pass, :hostname, :dbname, :db, and :sock will be deprecated at some point in the future."
         warn "Instead, please use :username, :password, :host, :port, :database, :socket, :flags for the options."
@@ -57,7 +75,6 @@ module Mysql2
       port     = opts[:port]
       database = opts[:database] || opts[:dbname] || opts[:db]
       socket   = opts[:socket] || opts[:sock]
-      flags    = opts[:flags] ? opts[:flags] | @query_options[:connect_flags] : @query_options[:connect_flags]
 
       # Correct the data types before passing these values down to the C level
       user = user.to_s unless user.nil?
@@ -70,8 +87,30 @@ module Mysql2
       connect user, pass, host, port, database, socket, flags
     end
 
-    def self.default_query_options
-      @@default_query_options
+    def parse_flags_array(flags, initial = 0)
+      flags.reduce(initial) do |memo, f|
+        fneg = f.start_with?('-') ? f[1..-1] : nil
+        if fneg && fneg =~ /^\w+$/ && Mysql2::Client.const_defined?(fneg)
+          memo & ~ Mysql2::Client.const_get(fneg)
+        elsif f && f =~ /^\w+$/ && Mysql2::Client.const_defined?(f)
+          memo | Mysql2::Client.const_get(f)
+        else
+          warn "Unknown MySQL connection flag: '#{f}'"
+          memo
+        end
+      end
+    end
+
+    if Thread.respond_to?(:handle_interrupt)
+      def query(sql, options = {})
+        Thread.handle_interrupt(::Mysql2::Util::TimeoutError => :never) do
+          _query(sql, @query_options.merge(options))
+        end
+      end
+    else
+      def query(sql, options = {})
+        _query(sql, @query_options.merge(options))
+      end
     end
 
     def query_info
@@ -82,9 +121,16 @@ module Mysql2
       info_hash
     end
 
-    private
-      def self.local_offset
+    def info
+      self.class.info
+    end
+
+    class << self
+      private
+
+      def local_offset
         ::Time.local(2010).utc_offset.to_r / 86400
       end
+    end
   end
 end
diff --git a/lib/mysql2/console.rb b/lib/mysql2/console.rb
index cad8243..d8fb9e3 100644
--- a/lib/mysql2/console.rb
+++ b/lib/mysql2/console.rb
@@ -1,5 +1,5 @@
 # Loaded by script/console. Land helpers here.
 
-Pry.config.prompt = lambda do |context, nesting, pry|
+Pry.config.prompt = lambda do |context, *|
   "[mysql2] #{context}> "
 end
diff --git a/lib/mysql2/em.rb b/lib/mysql2/em.rb
index b212659..b4210f0 100644
--- a/lib/mysql2/em.rb
+++ b/lib/mysql2/em.rb
@@ -17,7 +17,7 @@ module Mysql2
           detach
           begin
             result = @client.async_result
-          rescue Exception => e
+          rescue => e
             @deferable.fail(e)
           else
             @deferable.succeed(result)
@@ -34,17 +34,16 @@ module Mysql2
       end
 
       def close(*args)
-        if @watch
-          @watch.detach if @watch.watching?
-        end
+        @watch.detach if @watch && @watch.watching?
+
         super(*args)
       end
 
-      def query(sql, opts={})
+      def query(sql, opts = {})
         if ::EM.reactor_running?
           super(sql, opts.merge(:async => true))
           deferable = ::EM::DefaultDeferrable.new
-          @watch = ::EM.watch(self.socket, Watcher, self, deferable)
+          @watch = ::EM.watch(socket, Watcher, self, deferable)
           @watch.notify_readable = true
           deferable
         else
diff --git a/lib/mysql2/error.rb b/lib/mysql2/error.rb
index 8e6ed5b..f6ffcc4 100644
--- a/lib/mysql2/error.rb
+++ b/lib/mysql2/error.rb
@@ -2,25 +2,31 @@
 
 module Mysql2
   class Error < StandardError
-    REPLACEMENT_CHAR = '?'
-    ENCODE_OPTS      = {:undef => :replace, :invalid => :replace, :replace => REPLACEMENT_CHAR}
+    ENCODE_OPTS = {
+      :undef => :replace,
+      :invalid => :replace,
+      :replace => '?'.freeze,
+    }.freeze
 
-    attr_accessor :error_number
-    attr_reader   :sql_state
-    attr_writer   :server_version
+    attr_reader :error_number, :sql_state
 
     # Mysql gem compatibility
     alias_method :errno, :error_number
     alias_method :error, :message
 
-    def initialize(msg, server_version=nil)
-      self.server_version = server_version
+    def initialize(msg)
+      @server_version ||= nil
 
       super(clean_message(msg))
     end
 
-    def sql_state=(state)
-      @sql_state = ''.respond_to?(:encode) ? state.encode(ENCODE_OPTS) : state
+    def self.new_with_args(msg, server_version, error_number, sql_state)
+      err = allocate
+      err.instance_variable_set('@server_version', server_version)
+      err.instance_variable_set('@error_number', error_number)
+      err.instance_variable_set('@sql_state', sql_state.respond_to?(:encode) ? sql_state.encode(ENCODE_OPTS) : sql_state)
+      err.send(:initialize, msg)
+      err
     end
 
     private
@@ -30,7 +36,7 @@ module Mysql2
     # variable.
     #
     # See http://dev.mysql.com/doc/refman/5.5/en/charset-errors.html for
-    # more contetx.
+    # more context.
     #
     # Before MySQL 5.5 error message template strings are in whatever encoding
     # is associated with the error message language.
@@ -53,27 +59,12 @@ module Mysql2
     #
     # Returns a valid UTF-8 string in Ruby 1.9+, the original string on Ruby 1.8
     def clean_message(message)
-      return message if !message.respond_to?(:encoding)
+      return message unless message.respond_to?(:encode)
 
       if @server_version && @server_version > 50500
         message.encode(ENCODE_OPTS)
       else
-        if message.respond_to? :scrub
-          message.scrub(REPLACEMENT_CHAR).encode(ENCODE_OPTS)
-        else
-          # This is ugly as hell but Ruby 1.9 doesn't provide a way to clean a string
-          # and retain it's valid UTF-8 characters, that I know of.
-
-          new_message = "".force_encoding(Encoding::UTF_8)
-          message.chars.each do |char|
-            if char.valid_encoding?
-              new_message << char
-            else
-              new_message << REPLACEMENT_CHAR
-            end
-          end
-          new_message.encode(ENCODE_OPTS)
-        end
+        message.encode(Encoding::UTF_8, ENCODE_OPTS)
       end
     end
   end
diff --git a/lib/mysql2/field.rb b/lib/mysql2/field.rb
new file mode 100644
index 0000000..516ec17
--- /dev/null
+++ b/lib/mysql2/field.rb
@@ -0,0 +1,3 @@
+module Mysql2
+  Field = Struct.new(:name, :type)
+end
diff --git a/lib/mysql2/statement.rb b/lib/mysql2/statement.rb
new file mode 100644
index 0000000..f392c6e
--- /dev/null
+++ b/lib/mysql2/statement.rb
@@ -0,0 +1,17 @@
+module Mysql2
+  class Statement
+    include Enumerable
+
+    if Thread.respond_to?(:handle_interrupt)
+      def execute(*args)
+        Thread.handle_interrupt(::Mysql2::Util::TimeoutError => :never) do
+          _execute(*args)
+        end
+      end
+    else
+      def execute(*args)
+        _execute(*args)
+      end
+    end
+  end
+end
diff --git a/lib/mysql2/version.rb b/lib/mysql2/version.rb
index 303c549..e97480b 100644
--- a/lib/mysql2/version.rb
+++ b/lib/mysql2/version.rb
@@ -1,3 +1,3 @@
 module Mysql2
-  VERSION = "0.3.18"
+  VERSION = "0.4.2"
 end
diff --git a/metadata.yml b/metadata.yml
index 9f43fd4..f1ef27f 100644
--- a/metadata.yml
+++ b/metadata.yml
@@ -1,64 +1,27 @@
 --- !ruby/object:Gem::Specification
 name: mysql2
 version: !ruby/object:Gem::Version
-  version: 0.3.18
+  version: 0.4.2
 platform: ruby
 authors:
 - Brian Lopez
+- Aaron Stone
 autorequire: 
 bindir: bin
 cert_chain: []
-date: 2015-02-17 00:00:00.000000000 Z
-dependencies:
-- !ruby/object:Gem::Dependency
-  name: rake-compiler
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - ~>
-      - !ruby/object:Gem::Version
-        version: 0.9.5
-  type: :development
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - ~>
-      - !ruby/object:Gem::Version
-        version: 0.9.5
-- !ruby/object:Gem::Dependency
-  name: rake
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - ~>
-      - !ruby/object:Gem::Version
-        version: 0.9.3
-  type: :development
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - ~>
-      - !ruby/object:Gem::Version
-        version: 0.9.3
-- !ruby/object:Gem::Dependency
-  name: rspec
-  requirement: !ruby/object:Gem::Requirement
-    requirements:
-    - - ~>
-      - !ruby/object:Gem::Version
-        version: 2.8.0
-  type: :development
-  prerelease: false
-  version_requirements: !ruby/object:Gem::Requirement
-    requirements:
-    - - ~>
-      - !ruby/object:Gem::Version
-        version: 2.8.0
+date: 2015-11-25 00:00:00.000000000 Z
+dependencies: []
 description: 
-email: seniorlopez at gmail.com
+email:
+- seniorlopez at gmail.com
+- aaron at serendipity.cx
 executables: []
 extensions:
 - ext/mysql2/extconf.rb
 extra_rdoc_files: []
 files:
+- CHANGELOG.md
+- LICENSE
 - README.md
 - ext/mysql2/client.c
 - ext/mysql2/client.h
@@ -71,13 +34,17 @@ files:
 - ext/mysql2/mysql_enc_to_ruby.h
 - ext/mysql2/result.c
 - ext/mysql2/result.h
+- ext/mysql2/statement.c
+- ext/mysql2/statement.h
 - ext/mysql2/wait_for_single_fd.h
 - lib/mysql2.rb
 - lib/mysql2/client.rb
 - lib/mysql2/console.rb
 - lib/mysql2/em.rb
 - lib/mysql2/error.rb
+- lib/mysql2/field.rb
 - lib/mysql2/result.rb
+- lib/mysql2/statement.rb
 - lib/mysql2/version.rb
 - support/libmysql.def
 - support/mysql_enc_to_ruby.rb
@@ -90,8 +57,22 @@ files:
 - spec/mysql2/client_spec.rb
 - spec/mysql2/error_spec.rb
 - spec/mysql2/result_spec.rb
+- spec/mysql2/statement_spec.rb
 - spec/rcov.opts
 - spec/spec_helper.rb
+- spec/ssl/ca-cert.pem
+- spec/ssl/ca-key.pem
+- spec/ssl/ca.cnf
+- spec/ssl/cert.cnf
+- spec/ssl/client-cert.pem
+- spec/ssl/client-key.pem
+- spec/ssl/client-req.pem
+- spec/ssl/gen_certs.sh
+- spec/ssl/pkcs8-client-key.pem
+- spec/ssl/pkcs8-server-key.pem
+- spec/ssl/server-cert.pem
+- spec/ssl/server-key.pem
+- spec/ssl/server-req.pem
 - spec/test_data
 homepage: http://github.com/brianmario/mysql2
 licenses:
@@ -127,6 +108,20 @@ test_files:
 - spec/mysql2/client_spec.rb
 - spec/mysql2/error_spec.rb
 - spec/mysql2/result_spec.rb
+- spec/mysql2/statement_spec.rb
 - spec/rcov.opts
 - spec/spec_helper.rb
+- spec/ssl/ca-cert.pem
+- spec/ssl/ca-key.pem
+- spec/ssl/ca.cnf
+- spec/ssl/cert.cnf
+- spec/ssl/client-cert.pem
+- spec/ssl/client-key.pem
+- spec/ssl/client-req.pem
+- spec/ssl/gen_certs.sh
+- spec/ssl/pkcs8-client-key.pem
+- spec/ssl/pkcs8-server-key.pem
+- spec/ssl/server-cert.pem
+- spec/ssl/server-key.pem
+- spec/ssl/server-req.pem
 - spec/test_data
diff --git a/spec/em/em_spec.rb b/spec/em/em_spec.rb
index 01d162b..fbb54d6 100644
--- a/spec/em/em_spec.rb
+++ b/spec/em/em_spec.rb
@@ -4,7 +4,7 @@ begin
   require 'eventmachine'
   require 'mysql2/em'
 
-  describe Mysql2::EM::Client do
+  RSpec.describe Mysql2::EM::Client do
     it "should support async queries" do
       results = []
       EM.run do
@@ -24,8 +24,8 @@ begin
         end
       end
 
-      results[0].keys.should include("second_query")
-      results[1].keys.should include("first_query")
+      expect(results[0].keys).to include("second_query")
+      expect(results[1].keys).to include("first_query")
     end
 
     it "should support queries in callbacks" do
@@ -44,38 +44,38 @@ begin
         end
       end
 
-      results[0].keys.should include("first_query")
-      results[1].keys.should include("second_query")
+      expect(results[0].keys).to include("first_query")
+      expect(results[1].keys).to include("second_query")
     end
 
     it "should not swallow exceptions raised in callbacks" do
-      lambda {
+      expect {
         EM.run do
           client = Mysql2::EM::Client.new DatabaseCredentials['root']
           defer = client.query "SELECT sleep(0.1) as first_query"
-          defer.callback do |result|
+          defer.callback do
             client.close
-            raise 'some error'
+            fail 'some error'
           end
-          defer.errback do |err|
+          defer.errback do
             # This _shouldn't_ be run, but it needed to prevent the specs from
             # freezing if this test fails.
             EM.stop_event_loop
           end
         end
-      }.should raise_error
+      }.to raise_error('some error')
     end
 
     context 'when an exception is raised by the client' do
       let(:client) { Mysql2::EM::Client.new DatabaseCredentials['root'] }
       let(:error) { StandardError.new('some error') }
-      before { client.stub(:async_result).and_raise(error) }
+      before { allow(client).to receive(:async_result).and_raise(error) }
 
       it "should swallow exceptions raised in by the client" do
         errors = []
         EM.run do
           defer = client.query "SELECT sleep(0.1) as first_query"
-          defer.callback do |result|
+          defer.callback do
             # This _shouldn't_ be run, but it is needed to prevent the specs from
             # freezing if this test fails.
             EM.stop_event_loop
@@ -85,7 +85,7 @@ begin
             EM.stop_event_loop
           end
         end
-        errors.should == [error]
+        expect(errors).to eq([error])
       end
 
       it "should fail the deferrable" do
@@ -93,19 +93,19 @@ begin
         EM.run do
           defer = client.query "SELECT sleep(0.025) as first_query"
           EM.add_timer(0.1) do
-            defer.callback do |result|
+            defer.callback do
               callbacks_run << :callback
               # This _shouldn't_ be run, but it is needed to prevent the specs from
               # freezing if this test fails.
               EM.stop_event_loop
             end
-            defer.errback do |err|
+            defer.errback do
               callbacks_run << :errback
               EM.stop_event_loop
             end
           end
         end
-        callbacks_run.should == [:errback]
+        expect(callbacks_run).to eq([:errback])
       end
     end
 
@@ -114,17 +114,17 @@ begin
       EM.run do
         client = Mysql2::EM::Client.new DatabaseCredentials['root']
         defer = client.query("select sleep(0.025)")
-        defer.callback do |result|
+        defer.callback do
           callbacks_run << :callback
         end
-        defer.errback do |err|
+        defer.errback do
           callbacks_run << :errback
         end
         EM.add_timer(0.1) do
-          callbacks_run.should == [:callback]
-          lambda {
+          expect(callbacks_run).to eq([:callback])
+          expect {
             client.close
-          }.should_not raise_error(/invalid binding to detach/)
+          }.not_to raise_error
           EM.stop_event_loop
         end
       end
diff --git a/spec/mysql2/client_spec.rb b/spec/mysql2/client_spec.rb
index 27aaa69..a6e9bcd 100644
--- a/spec/mysql2/client_spec.rb
+++ b/spec/mysql2/client_spec.rb
@@ -1,77 +1,82 @@
 # encoding: UTF-8
 require 'spec_helper'
 
-describe Mysql2::Client do
+RSpec.describe Mysql2::Client do
   context "using defaults file" do
     let(:cnf_file) { File.expand_path('../../my.cnf', __FILE__) }
 
     it "should not raise an exception for valid defaults group" do
-      lambda {
+      expect {
         opts = DatabaseCredentials['root'].merge(:default_file => cnf_file, :default_group => "test")
         @client = Mysql2::Client.new(opts)
-      }.should_not raise_error(Mysql2::Error)
+      }.not_to raise_error
     end
 
     it "should not raise an exception without default group" do
-      lambda {
+      expect {
         @client = Mysql2::Client.new(DatabaseCredentials['root'].merge(:default_file => cnf_file))
-      }.should_not raise_error(Mysql2::Error)
+      }.not_to raise_error
     end
   end
 
   it "should raise an exception upon connection failure" do
-    lambda {
+    expect {
       # The odd local host IP address forces the mysql client library to
       # use a TCP socket rather than a domain socket.
       Mysql2::Client.new DatabaseCredentials['root'].merge('host' => '127.0.0.2', 'port' => 999999)
-    }.should raise_error(Mysql2::Error)
+    }.to raise_error(Mysql2::Error)
   end
 
-  if defined? Encoding
-    it "should raise an exception on create for invalid encodings" do
-      lambda {
-        Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => "fake"))
-      }.should raise_error(Mysql2::Error)
-    end
+  it "should raise an exception on create for invalid encodings" do
+    expect {
+      Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => "fake"))
+    }.to raise_error(Mysql2::Error)
+  end
 
-    it "should not raise an exception on create for a valid encoding" do
-      lambda {
-        Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => "utf8"))
-      }.should_not raise_error(Mysql2::Error)
+  it "should not raise an exception on create for a valid encoding" do
+    expect {
+      Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => "utf8"))
+    }.not_to raise_error
 
-      lambda {
-        Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => "big5"))
-      }.should_not raise_error(Mysql2::Error)
+    expect {
+      Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => "big5"))
+    }.not_to raise_error
+  end
+
+  Klient = Class.new(Mysql2::Client) do
+    attr_reader :connect_args
+    def connect(*args)
+      @connect_args ||= []
+      @connect_args << args
     end
   end
 
   it "should accept connect flags and pass them to #connect" do
-    klient = Class.new(Mysql2::Client) do
-      attr_reader :connect_args
-      def connect *args
-        @connect_args ||= []
-        @connect_args << args
-      end
-    end
-    client = klient.new :flags => Mysql2::Client::FOUND_ROWS
-    (client.connect_args.last[6] & Mysql2::Client::FOUND_ROWS).should be_true
+    client = Klient.new :flags => Mysql2::Client::FOUND_ROWS
+    expect(client.connect_args.last[6] & Mysql2::Client::FOUND_ROWS).to be > 0
+  end
+
+  it "should parse flags array" do
+    client = Klient.new :flags => %w( FOUND_ROWS -PROTOCOL_41 )
+    expect(client.connect_args.last[6] & Mysql2::Client::FOUND_ROWS).to eql(Mysql2::Client::FOUND_ROWS)
+    expect(client.connect_args.last[6] & Mysql2::Client::PROTOCOL_41).to eql(0)
+  end
+
+  it "should parse flags string" do
+    client = Klient.new :flags => "FOUND_ROWS -PROTOCOL_41"
+    expect(client.connect_args.last[6] & Mysql2::Client::FOUND_ROWS).to eql(Mysql2::Client::FOUND_ROWS)
+    expect(client.connect_args.last[6] & Mysql2::Client::PROTOCOL_41).to eql(0)
   end
 
   it "should default flags to (REMEMBER_OPTIONS, LONG_PASSWORD, LONG_FLAG, TRANSACTIONS, PROTOCOL_41, SECURE_CONNECTION)" do
-    klient = Class.new(Mysql2::Client) do
-      attr_reader :connect_args
-      def connect *args
-        @connect_args ||= []
-        @connect_args << args
-      end
-    end
-    client = klient.new
-    (client.connect_args.last[6] & (Mysql2::Client::REMEMBER_OPTIONS |
-                                     Mysql2::Client::LONG_PASSWORD |
-                                     Mysql2::Client::LONG_FLAG |
-                                     Mysql2::Client::TRANSACTIONS |
-                                     Mysql2::Client::PROTOCOL_41 |
-                                     Mysql2::Client::SECURE_CONNECTION)).should be_true
+    client = Klient.new
+    client_flags = Mysql2::Client::REMEMBER_OPTIONS |
+                   Mysql2::Client::LONG_PASSWORD |
+                   Mysql2::Client::LONG_FLAG |
+                   Mysql2::Client::TRANSACTIONS |
+                   Mysql2::Client::PROTOCOL_41 |
+                   Mysql2::Client::SECURE_CONNECTION
+    expect(client.connect_args.last[6]).to eql(client_flags)
   end
 
   it "should execute init command" do
@@ -79,7 +84,7 @@ describe Mysql2::Client do
     options[:init_command] = "SET @something = 'setting_value';"
     client = Mysql2::Client.new(options)
     result = client.query("SELECT @something;")
-    result.first['@something'].should eq('setting_value')
+    expect(result.first['@something']).to eq('setting_value')
   end
 
   it "should send init_command after reconnect" do
@@ -89,17 +94,14 @@ describe Mysql2::Client do
     client = Mysql2::Client.new(options)
 
     result = client.query("SELECT @something;")
-    result.first['@something'].should eq('setting_value')
+    expect(result.first['@something']).to eq('setting_value')
 
     # get the current connection id
     result = client.query("SELECT CONNECTION_ID()")
     first_conn_id = result.first['CONNECTION_ID()']
 
     # break the current connection
-    begin
-      client.query("KILL #{first_conn_id}")
-    rescue Mysql2::Error
-    end
+    expect { client.query("KILL #{first_conn_id}") }.to raise_error(Mysql2::Error)
 
     client.ping # reconnect now
 
@@ -108,53 +110,75 @@ describe Mysql2::Client do
     second_conn_id = result.first['CONNECTION_ID()']
 
     # confirm reconnect by checking the new connection id
-    first_conn_id.should_not == second_conn_id
+    expect(first_conn_id).not_to eq(second_conn_id)
 
     # At last, check that the init command executed
     result = client.query("SELECT @something;")
-    result.first['@something'].should eq('setting_value')
+    expect(result.first['@something']).to eq('setting_value')
   end
 
   it "should have a global default_query_options hash" do
-    Mysql2::Client.should respond_to(:default_query_options)
+    expect(Mysql2::Client).to respond_to(:default_query_options)
   end
 
   it "should be able to connect via SSL options" do
     ssl = @client.query "SHOW VARIABLES LIKE 'have_ssl'"
-    ssl_uncompiled = ssl.any? {|x| x['Value'] == 'OFF'}
+    ssl_uncompiled = ssl.any? { |x| x['Value'] == 'OFF' }
     pending("DON'T WORRY, THIS TEST PASSES - but SSL is not compiled into your MySQL daemon.") if ssl_uncompiled
-    ssl_disabled = ssl.any? {|x| x['Value'] == 'DISABLED'}
+    ssl_disabled = ssl.any? { |x| x['Value'] == 'DISABLED' }
     pending("DON'T WORRY, THIS TEST PASSES - but SSL is not enabled in your MySQL daemon.") if ssl_disabled
 
     # You may need to adjust the lines below to match your SSL certificate paths
     ssl_client = nil
-    lambda {
+    expect {
+      # rubocop:disable Style/TrailingComma
       ssl_client = Mysql2::Client.new(
-        :sslkey    => '/etc/mysql/client-key.pem',
-        :sslcert   => '/etc/mysql/client-cert.pem',
-        :sslca     => '/etc/mysql/ca-cert.pem',
-        :sslcapath => '/etc/mysql/',
-        :sslcipher => 'DHE-RSA-AES256-SHA'
+        DatabaseCredentials['root'].merge(
+          'host'     => 'mysql2gem.example.com', # must match the certificates
+          :sslkey    => '/etc/mysql/client-key.pem',
+          :sslcert   => '/etc/mysql/client-cert.pem',
+          :sslca     => '/etc/mysql/ca-cert.pem',
+          :sslcipher => 'DHE-RSA-AES256-SHA',
+          :sslverify => true
+        )
       )
-    }.should_not raise_error(Mysql2::Error)
+      # rubocop:enable Style/TrailingComma
+    }.not_to raise_error
 
     results = ssl_client.query("SHOW STATUS WHERE Variable_name = \"Ssl_version\" OR Variable_name = \"Ssl_cipher\"").to_a
-    results[0]['Variable_name'].should eql('Ssl_cipher')
-    results[0]['Value'].should_not be_nil
-    results[0]['Value'].should be_kind_of(String)
-    results[0]['Value'].should_not be_empty
+    expect(results[0]['Variable_name']).to eql('Ssl_cipher')
+    expect(results[0]['Value']).not_to be_nil
+    expect(results[0]['Value']).to be_an_instance_of(String)
+    expect(results[0]['Value']).not_to be_empty
 
-    results[1]['Variable_name'].should eql('Ssl_version')
-    results[1]['Value'].should_not be_nil
-    results[1]['Value'].should be_kind_of(String)
-    results[1]['Value'].should_not be_empty
+    expect(results[1]['Variable_name']).to eql('Ssl_version')
+    expect(results[1]['Value']).not_to be_nil
+    expect(results[1]['Value']).to be_an_instance_of(String)
+    expect(results[1]['Value']).not_to be_empty
 
     ssl_client.close
   end
 
+  def run_gc
+    if defined?(Rubinius)
+      GC.run(true)
+    else
+      GC.start
+    end
+    sleep(0.5)
+  end
+
+  it "should terminate connections when calling close" do
+    expect {
+      Mysql2::Client.new(DatabaseCredentials['root']).close
+    }.to_not change {
+      @client.query("SHOW STATUS LIKE 'Aborted_clients'").first['Value'].to_i
+    }
+  end
+
   it "should not leave dangling connections after garbage collection" do
-    GC.start
-    sleep 0.300 # Let GC do its work
+    run_gc
+
     client = Mysql2::Client.new(DatabaseCredentials['root'])
     before_count = client.query("SHOW STATUS LIKE 'Threads_connected'").first['Value'].to_i
 
@@ -162,69 +186,70 @@ describe Mysql2::Client do
       Mysql2::Client.new(DatabaseCredentials['root']).query('SELECT 1')
     end
     after_count = client.query("SHOW STATUS LIKE 'Threads_connected'").first['Value'].to_i
-    after_count.should == before_count + 10
+    expect(after_count).to eq(before_count + 10)
 
-    GC.start
-    sleep 0.300 # Let GC do its work
+    run_gc
     final_count = client.query("SHOW STATUS LIKE 'Threads_connected'").first['Value'].to_i
-    final_count.should == before_count
+    expect(final_count).to eq(before_count)
   end
 
-  if Process.respond_to?(:fork)
-    it "should not close connections when running in a child process" do
-      GC.start
-      sleep 1 if defined? Rubinius # Let the rbx GC thread do its work
-      client = Mysql2::Client.new(DatabaseCredentials['root'])
-
-      fork do
-        client.query('SELECT 1')
-        client = nil
-        GC.start
-        sleep 1 if defined? Rubinius # Let the rbx GC thread do its work
-      end
+  it "should not close connections when running in a child process" do
+    pending("fork is not available on this platform") unless Process.respond_to?(:fork)
 
-      Process.wait
+    run_gc
+    client = Mysql2::Client.new(DatabaseCredentials['root'])
+
+    # this empty `fork` call fixes this tests on RBX; without it, the next
+    # `fork` call hangs forever. WTF?
+    fork {}
 
-      # this will throw an error if the underlying socket was shutdown by the
-      # child's GC
-      expect { client.query('SELECT 1') }.to_not raise_exception
+    fork do
+      client.query('SELECT 1')
+      client = nil
+      run_gc
     end
+
+    Process.wait
+
+    # this will throw an error if the underlying socket was shutdown by the
+    # child's GC
+    expect { client.query('SELECT 1') }.to_not raise_exception
   end
 
   it "should be able to connect to database with numeric-only name" do
-    lambda {
-      creds = DatabaseCredentials['numericuser']
-      @client.query "CREATE DATABASE IF NOT EXISTS `#{creds['database']}`"
-      @client.query "GRANT ALL ON `#{creds['database']}`.* TO #{creds['username']}@`#{creds['host']}`"
-      client = Mysql2::Client.new creds
-      @client.query "DROP DATABASE IF EXISTS `#{creds['database']}`"
-    }.should_not raise_error
+    creds = DatabaseCredentials['numericuser']
+    @client.query "CREATE DATABASE IF NOT EXISTS `#{creds['database']}`"
+    @client.query "GRANT ALL ON `#{creds['database']}`.* TO #{creds['username']}@`#{creds['host']}`"
+
+    expect { Mysql2::Client.new(creds) }.not_to raise_error
+
+    @client.query "DROP DATABASE IF EXISTS `#{creds['database']}`"
   end
 
   it "should respond to #close" do
-    @client.should respond_to(:close)
+    expect(@client).to respond_to(:close)
   end
 
   it "should be able to close properly" do
-    @client.close.should be_nil
-    lambda {
+    expect(@client.close).to be_nil
+    expect {
       @client.query "SELECT 1"
-    }.should raise_error(Mysql2::Error)
+    }.to raise_error(Mysql2::Error)
   end
 
   it "should respond to #query" do
-    @client.should respond_to(:query)
+    expect(@client).to respond_to(:query)
   end
 
   it "should respond to #warning_count" do
-    @client.should respond_to(:warning_count)
+    expect(@client).to respond_to(:warning_count)
   end
 
   context "#warning_count" do
     context "when no warnings" do
       it "should 0" do
         @client.query('select 1')
-        @client.warning_count.should == 0
+        expect(@client.warning_count).to eq(0)
       end
     end
     context "when has a warnings" do
@@ -232,21 +257,21 @@ describe Mysql2::Client do
         # "the statement produces extra information that can be viewed by issuing a SHOW WARNINGS"
         # http://dev.mysql.com/doc/refman/5.0/en/explain-extended.html
         @client.query("explain extended select 1")
-        @client.warning_count.should > 0
+        expect(@client.warning_count).to be > 0
       end
     end
   end
 
   it "should respond to #query_info" do
-    @client.should respond_to(:query_info)
+    expect(@client).to respond_to(:query_info)
   end
 
   context "#query_info" do
     context "when no info present" do
       it "should 0" do
         @client.query('select 1')
-        @client.query_info.should be_empty
-        @client.query_info_string.should be_nil
+        expect(@client.query_info).to be_empty
+        expect(@client.query_info_string).to be_nil
       end
     end
     context "when has some info" do
@@ -258,8 +283,8 @@ describe Mysql2::Client do
         # # Note that mysql_info() returns a non-NULL value for INSERT ... VALUES only for the multiple-row form of the statement (that is, only if multiple value lists are specified).
         @client.query("INSERT INTO infoTest (blah) VALUES (1234),(4535)")
 
-        @client.query_info.should  eql({:records => 2, :duplicates => 0, :warnings => 0})
-        @client.query_info_string.should eq('Records: 2  Duplicates: 0  Warnings: 0')
+        expect(@client.query_info).to eql(:records => 2, :duplicates => 0, :warnings => 0)
+        expect(@client.query_info_string).to eq('Records: 2  Duplicates: 0  Warnings: 0')
 
         @client.query "DROP TABLE infoTest"
       end
@@ -270,7 +295,7 @@ describe Mysql2::Client do
     before(:all) do
       @client_i = Mysql2::Client.new DatabaseCredentials['root'].merge(:local_infile => true)
       local = @client_i.query "SHOW VARIABLES LIKE 'local_infile'"
-      local_enabled = local.any? {|x| x['Value'] == 'ON'}
+      local_enabled = local.any? { |x| x['Value'] == 'ON' }
       pending("DON'T WORRY, THIS TEST PASSES - but LOCAL INFILE is not enabled in your MySQL daemon.") unless local_enabled
 
       @client_i.query %[
@@ -288,43 +313,43 @@ describe Mysql2::Client do
 
     it "should raise an error when local_infile is disabled" do
       client = Mysql2::Client.new DatabaseCredentials['root'].merge(:local_infile => false)
-      lambda {
+      expect {
         client.query "LOAD DATA LOCAL INFILE 'spec/test_data' INTO TABLE infileTest"
-      }.should raise_error(Mysql2::Error, %r{command is not allowed})
+      }.to raise_error(Mysql2::Error, /command is not allowed/)
     end
 
     it "should raise an error when a non-existent file is loaded" do
-      lambda {
+      expect {
         @client_i.query "LOAD DATA LOCAL INFILE 'this/file/is/not/here' INTO TABLE infileTest"
-      }.should_not raise_error(Mysql2::Error, %r{file not found: this/file/is/not/here})
+      }.to raise_error(Mysql2::Error, 'No such file or directory: this/file/is/not/here')
     end
 
     it "should LOAD DATA LOCAL INFILE" do
       @client_i.query "LOAD DATA LOCAL INFILE 'spec/test_data' INTO TABLE infileTest"
       info = @client_i.query_info
-      info.should eql({:records => 1, :deleted => 0, :skipped => 0, :warnings => 0})
+      expect(info).to eql(:records => 1, :deleted => 0, :skipped => 0, :warnings => 0)
 
       result = @client_i.query "SELECT * FROM infileTest"
-      result.first.should eql({'id' => 1, 'foo' => 'Hello', 'bar' => 'World'})
+      expect(result.first).to eql('id' => 1, 'foo' => 'Hello', 'bar' => 'World')
     end
   end
 
   it "should expect connect_timeout to be a positive integer" do
-    lambda {
+    expect {
       Mysql2::Client.new(:connect_timeout => -1)
-    }.should raise_error(Mysql2::Error)
+    }.to raise_error(Mysql2::Error)
   end
 
   it "should expect read_timeout to be a positive integer" do
-    lambda {
+    expect {
       Mysql2::Client.new(:read_timeout => -1)
-    }.should raise_error(Mysql2::Error)
+    }.to raise_error(Mysql2::Error)
   end
 
   it "should expect write_timeout to be a positive integer" do
-    lambda {
+    expect {
       Mysql2::Client.new(:write_timeout => -1)
-    }.should raise_error(Mysql2::Error)
+    }.to raise_error(Mysql2::Error)
   end
 
   context "#query" do
@@ -333,7 +358,7 @@ describe Mysql2::Client do
 
       expect {
         @client.query("SELECT 1 UNION SELECT 2", :stream => true, :cache_rows => false)
-      }.to_not raise_exception(Mysql2::Error)
+      }.to_not raise_error
     end
 
     it "should not let you query again if iterating is not finished when streaming" do
@@ -345,200 +370,196 @@ describe Mysql2::Client do
     end
 
     it "should only accept strings as the query parameter" do
-      lambda {
+      expect {
         @client.query ["SELECT 'not right'"]
-      }.should raise_error(TypeError)
+      }.to raise_error(TypeError)
     end
 
     it "should not retain query options set on a query for subsequent queries, but should retain it in the result" do
       result = @client.query "SELECT 1", :something => :else
-      @client.query_options[:something].should be_nil
-      result.instance_variable_get('@query_options').should eql(@client.query_options.merge(:something => :else))
-      @client.instance_variable_get('@current_query_options').should eql(@client.query_options.merge(:something => :else))
+      expect(@client.query_options[:something]).to be_nil
+      expect(result.instance_variable_get('@query_options')).to eql(@client.query_options.merge(:something => :else))
+      expect(@client.instance_variable_get('@current_query_options')).to eql(@client.query_options.merge(:something => :else))
 
       result = @client.query "SELECT 1"
-      result.instance_variable_get('@query_options').should eql(@client.query_options)
-      @client.instance_variable_get('@current_query_options').should eql(@client.query_options)
+      expect(result.instance_variable_get('@query_options')).to eql(@client.query_options)
+      expect(@client.instance_variable_get('@current_query_options')).to eql(@client.query_options)
     end
 
     it "should allow changing query options for subsequent queries" do
       @client.query_options.merge!(:something => :else)
       result = @client.query "SELECT 1"
-      @client.query_options[:something].should eql(:else)
-      result.instance_variable_get('@query_options')[:something].should eql(:else)
+      expect(@client.query_options[:something]).to eql(:else)
+      expect(result.instance_variable_get('@query_options')[:something]).to eql(:else)
 
       # Clean up after this test
       @client.query_options.delete(:something)
-      @client.query_options[:something].should be_nil
+      expect(@client.query_options[:something]).to be_nil
     end
 
     it "should return results as a hash by default" do
-      @client.query("SELECT 1").first.class.should eql(Hash)
+      expect(@client.query("SELECT 1").first).to be_an_instance_of(Hash)
     end
 
     it "should be able to return results as an array" do
-      @client.query("SELECT 1", :as => :array).first.class.should eql(Array)
+      expect(@client.query("SELECT 1", :as => :array).first).to be_an_instance_of(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)
+      expect(@client.query("SELECT 1", :symbolize_keys => true).first.keys[0]).to be_an_instance_of(Symbol)
     end
 
     it "should require an open connection" do
       @client.close
-      lambda {
+      expect {
         @client.query "SELECT 1"
-      }.should raise_error(Mysql2::Error)
+      }.to raise_error(Mysql2::Error)
     end
 
     if RUBY_PLATFORM !~ /mingw|mswin/
       it "should not allow another query to be sent without fetching a result first" do
         @client.query("SELECT 1", :async => true)
-        lambda {
+        expect {
           @client.query("SELECT 1")
-        }.should raise_error(Mysql2::Error)
+        }.to raise_error(Mysql2::Error)
       end
 
       it "should describe the thread holding the active query" do
         thr = Thread.new { @client.query("SELECT 1", :async => true) }
 
         thr.join
-        begin
-          @client.query("SELECT 1")
-        rescue Mysql2::Error => e
-          message = e.message
-        end
-        re = Regexp.escape(thr.inspect)
-        message.should match(Regexp.new(re))
+        expect { @client.query('SELECT 1') }.to raise_error(Mysql2::Error, Regexp.new(Regexp.escape(thr.inspect)))
       end
 
       it "should timeout if we wait longer than :read_timeout" do
-        client = Mysql2::Client.new(DatabaseCredentials['root'].merge(:read_timeout => 1))
-        lambda {
-          client.query("SELECT sleep(2)")
-        }.should raise_error(Mysql2::Error)
+        client = Mysql2::Client.new(DatabaseCredentials['root'].merge(:read_timeout => 0))
+        expect {
+          client.query('SELECT SLEEP(0.1)')
+        }.to raise_error(Mysql2::Error)
       end
 
-      if !defined? Rubinius
-        # XXX this test is not deterministic (because Unix signal handling is not)
-        # and may fail on a loaded system
-        it "should run signal handlers while waiting for a response" do
-          mark = {}
-          trap(:USR1) { mark[:USR1] = Time.now }
-          begin
-            mark[:START] = Time.now
-            pid = fork do
-              sleep 1 # wait for client "SELECT sleep(2)" query to start
-              Process.kill(:USR1, Process.ppid)
-              sleep # wait for explicit kill to prevent GC disconnect
-            end
-            @client.query("SELECT sleep(2)")
-            mark[:END] = Time.now
-            mark.include?(:USR1).should be_true
-            (mark[:USR1] - mark[:START]).should >= 1
-            (mark[:USR1] - mark[:START]).should < 1.3
-            (mark[:END] - mark[:USR1]).should > 0.9
-            (mark[:END] - mark[:START]).should >= 2
-            (mark[:END] - mark[:START]).should < 2.3
-            Process.kill(:TERM, pid)
-            Process.waitpid2(pid)
-          ensure
-            trap(:USR1, 'DEFAULT')
+      # XXX this test is not deterministic (because Unix signal handling is not)
+      # and may fail on a loaded system
+      it "should run signal handlers while waiting for a response" do
+        kill_time = 0.1
+        query_time = 2 * kill_time
+
+        mark = {}
+
+        begin
+          trap(:USR1) { mark.store(:USR1, Time.now) }
+          pid = fork do
+            sleep kill_time # wait for client query to start
+            Process.kill(:USR1, Process.ppid)
+            sleep # wait for explicit kill to prevent GC disconnect
           end
+          mark.store(:QUERY_START, Time.now)
+          @client.query("SELECT SLEEP(#{query_time})")
+          mark.store(:QUERY_END, Time.now)
+        ensure
+          Process.kill(:TERM, pid)
+          Process.waitpid2(pid)
+          trap(:USR1, 'DEFAULT')
         end
+
+        # the query ran uninterrupted
+        expect(mark.fetch(:QUERY_END) - mark.fetch(:QUERY_START)).to be_within(0.02).of(query_time)
+        # signals fired while the query was running
+        expect(mark.fetch(:USR1)).to be_between(mark.fetch(:QUERY_START), mark.fetch(:QUERY_END))
       end
 
       it "#socket should return a Fixnum (file descriptor from C)" do
-        @client.socket.class.should eql(Fixnum)
-        @client.socket.should_not eql(0)
+        expect(@client.socket).to be_an_instance_of(Fixnum)
+        expect(@client.socket).not_to eql(0)
       end
 
       it "#socket should require an open connection" do
         @client.close
-        lambda {
+        expect {
           @client.socket
-        }.should raise_error(Mysql2::Error)
+        }.to raise_error(Mysql2::Error)
       end
 
-      it "should close the connection when an exception is raised" do
-        begin
-          Timeout.timeout(1, Timeout::Error) do
-            @client.query("SELECT sleep(2)")
-          end
-        rescue Timeout::Error
-        end
+      it 'should be impervious to connection-corrupting timeouts in #query' do
+        pending('`Thread.handle_interrupt` is not defined') unless Thread.respond_to?(:handle_interrupt)
+        # attempt to break the connection
+        expect { Timeout.timeout(0.1) { @client.query('SELECT SLEEP(0.2)') } }.to raise_error(Timeout::Error)
 
-        lambda {
-          @client.query("SELECT 1")
-        }.should raise_error(Mysql2::Error, 'closed MySQL connection')
+        # expect the connection to not be broken
+        expect { @client.query('SELECT 1') }.to_not raise_error
       end
 
-      it "should handle Timeouts without leaving the connection hanging if reconnect is true" do
-        client = Mysql2::Client.new(DatabaseCredentials['root'].merge(:reconnect => true))
-        begin
-          Timeout.timeout(1, Timeout::Error) do
-            client.query("SELECT sleep(2)")
-          end
-        rescue Timeout::Error
-        end
+      it 'should be impervious to connection-corrupting timeouts in #execute' do
+        # the statement handle gets corrupted and will segfault the tests if interrupted,
+        # so we can't even use pending on this test, really have to skip it on older Rubies.
+        skip('`Thread.handle_interrupt` is not defined') unless Thread.respond_to?(:handle_interrupt)
 
-        lambda {
-          client.query("SELECT 1")
-        }.should_not raise_error(Mysql2::Error)
+        # attempt to break the connection
+        stmt = @client.prepare('SELECT SLEEP(?)')
+        expect { Timeout.timeout(0.1) { stmt.execute(0.2) } }.to raise_error(Timeout::Error)
+        stmt.close
+
+        # expect the connection to not be broken
+        expect { @client.query('SELECT 1') }.to_not raise_error
       end
 
-      it "should handle Timeouts without leaving the connection hanging if reconnect is set to true after construction true" do
-        client = Mysql2::Client.new(DatabaseCredentials['root'])
-        begin
-          Timeout.timeout(1, Timeout::Error) do
-            client.query("SELECT sleep(2)")
-          end
-        rescue Timeout::Error
+      context 'when a non-standard exception class is raised' do
+        it "should close the connection when an exception is raised" do
+          expect { Timeout.timeout(0.1, ArgumentError) { @client.query('SELECT SLEEP(1)') } }.to raise_error(ArgumentError)
+          expect { @client.query('SELECT 1') }.to raise_error(Mysql2::Error, 'closed MySQL connection')
         end
 
-        lambda {
-          client.query("SELECT 1")
-        }.should raise_error(Mysql2::Error)
+        it "should handle Timeouts without leaving the connection hanging if reconnect is true" do
+          if RUBY_PLATFORM.include?('darwin') && Mysql2::Client.info.fetch(:version).start_with?('5.5')
+            pending('libmysqlclient 5.5 on OSX is afflicted by an unknown bug that breaks this test. See #633 and #634.')
+          end
 
-        client.reconnect = true
+          client = Mysql2::Client.new(DatabaseCredentials['root'].merge(:reconnect => true))
 
-        begin
-          Timeout.timeout(1, Timeout::Error) do
-            client.query("SELECT sleep(2)")
-          end
-        rescue Timeout::Error
+          expect { Timeout.timeout(0.1, ArgumentError) { client.query('SELECT SLEEP(1)') } }.to raise_error(ArgumentError)
+          expect { client.query('SELECT 1') }.to_not raise_error
         end
 
-        lambda {
-          client.query("SELECT 1")
-        }.should_not raise_error(Mysql2::Error)
+        it "should handle Timeouts without leaving the connection hanging if reconnect is set to true after construction" do
+          if RUBY_PLATFORM.include?('darwin') && Mysql2::Client.info.fetch(:version).start_with?('5.5')
+            pending('libmysqlclient 5.5 on OSX is afflicted by an unknown bug that breaks this test. See #633 and #634.')
+          end
+
+          client = Mysql2::Client.new(DatabaseCredentials['root'])
+
+          expect { Timeout.timeout(0.1, ArgumentError) { client.query('SELECT SLEEP(1)') } }.to raise_error(ArgumentError)
+          expect { client.query('SELECT 1') }.to raise_error(Mysql2::Error)
 
+          client.reconnect = true
+
+          expect { Timeout.timeout(0.1, ArgumentError) { client.query('SELECT SLEEP(1)') } }.to raise_error(ArgumentError)
+          expect { client.query('SELECT 1') }.to_not raise_error
+        end
       end
 
       it "threaded queries should be supported" do
-        threads, results = [], {}
-        lock = Mutex.new
-        connect = lambda{
-          Mysql2::Client.new(DatabaseCredentials['root'])
-        }
-        Timeout.timeout(0.7) do
-          5.times {
-            threads << Thread.new do
-              result = connect.call.query("SELECT sleep(0.5) as result")
-              lock.synchronize do
-                results[Thread.current.object_id] = result
-              end
-            end
-          }
+        sleep_time = 0.5
+
+        # Note that each thread opens its own database connection
+        threads = 5.times.map do
+          Thread.new do
+            client = Mysql2::Client.new(DatabaseCredentials.fetch('root'))
+            client.query("SELECT SLEEP(#{sleep_time})")
+            Thread.current.object_id
+          end
         end
-        threads.each{|t| t.join }
-        results.keys.sort.should eql(threads.map{|t| t.object_id }.sort)
+
+        # This timeout demonstrates that the threads are sleeping concurrently:
+        # In the serial case, the timeout would fire and the test would fail
+        values = Timeout.timeout(sleep_time * 1.1) { threads.map(&:value) }
+
+        expect(values).to match_array(threads.map(&:object_id))
       end
 
       it "evented async queries should be supported" do
         # should immediately return nil
-        @client.query("SELECT sleep(0.1)", :async => true).should eql(nil)
+        expect(@client.query("SELECT sleep(0.1)", :async => true)).to eql(nil)
 
         io_wrapper = IO.for_fd(@client.socket)
         loops = 0
@@ -551,10 +572,10 @@ describe Mysql2::Client do
         end
 
         # make sure we waited some period of time
-        (loops >= 1).should be_true
+        expect(loops >= 1).to be true
 
         result = @client.async_result
-        result.class.should eql(Mysql2::Result)
+        expect(result).to be_an_instance_of(Mysql2::Result)
       end
     end
 
@@ -565,230 +586,251 @@ describe Mysql2::Client do
 
       it "should raise an exception when one of multiple statements fails" do
         result = @multi_client.query("SELECT 1 AS 'set_1'; SELECT * FROM invalid_table_name; SELECT 2 AS 'set_2';")
-        result.first['set_1'].should be(1)
-        lambda {
+        expect(result.first['set_1']).to be(1)
+        expect {
           @multi_client.next_result
-        }.should raise_error(Mysql2::Error)
-        @multi_client.next_result.should be_false
+        }.to raise_error(Mysql2::Error)
+        expect(@multi_client.next_result).to be false
       end
 
       it "returns multiple result sets" do
-        @multi_client.query("SELECT 1 AS 'set_1'; SELECT 2 AS 'set_2'").first.should eql({ 'set_1' => 1 })
+        expect(@multi_client.query("SELECT 1 AS 'set_1'; SELECT 2 AS 'set_2'").first).to eql('set_1' => 1)
 
-        @multi_client.next_result.should be_true
-        @multi_client.store_result.first.should eql({ 'set_2' => 2 })
+        expect(@multi_client.next_result).to be true
+        expect(@multi_client.store_result.first).to eql('set_2' => 2)
 
-        @multi_client.next_result.should be_false
+        expect(@multi_client.next_result).to be false
       end
 
       it "does not interfere with other statements" do
         @multi_client.query("SELECT 1 AS 'set_1'; SELECT 2 AS 'set_2'")
-        while( @multi_client.next_result )
-          @multi_client.store_result
-        end
+        @multi_client.store_result while @multi_client.next_result
 
-        @multi_client.query("SELECT 3 AS 'next'").first.should == { 'next' => 3 }
+        expect(@multi_client.query("SELECT 3 AS 'next'").first).to eq('next' => 3)
       end
 
       it "will raise on query if there are outstanding results to read" do
         @multi_client.query("SELECT 1; SELECT 2; SELECT 3")
-        lambda {
+        expect {
           @multi_client.query("SELECT 4")
-        }.should raise_error(Mysql2::Error)
+        }.to raise_error(Mysql2::Error)
       end
 
       it "#abandon_results! should work" do
         @multi_client.query("SELECT 1; SELECT 2; SELECT 3")
         @multi_client.abandon_results!
-        lambda {
+        expect {
           @multi_client.query("SELECT 4")
-        }.should_not raise_error(Mysql2::Error)
+        }.not_to raise_error
       end
 
       it "#more_results? should work" do
         @multi_client.query("SELECT 1 AS 'set_1'; SELECT 2 AS 'set_2'")
-        @multi_client.more_results?.should be_true
+        expect(@multi_client.more_results?).to be true
 
         @multi_client.next_result
         @multi_client.store_result
 
-        @multi_client.more_results?.should be_false
+        expect(@multi_client.more_results?).to be false
+      end
+
+      it "#more_results? should work with stored procedures" do
+        @multi_client.query("DROP PROCEDURE IF EXISTS test_proc")
+        @multi_client.query("CREATE PROCEDURE test_proc() BEGIN SELECT 1 AS 'set_1'; SELECT 2 AS 'set_2'; END")
+        expect(@multi_client.query("CALL test_proc()").first).to eql('set_1' => 1)
+        expect(@multi_client.more_results?).to be true
+
+        @multi_client.next_result
+        expect(@multi_client.store_result.first).to eql('set_2' => 2)
+
+        @multi_client.next_result
+        expect(@multi_client.store_result).to be_nil # this is the result from CALL itself
+
+        expect(@multi_client.more_results?).to be false
       end
     end
   end
 
   it "should respond to #socket" do
-    @client.should respond_to(:socket)
+    expect(@client).to respond_to(:socket)
   end
 
   if RUBY_PLATFORM =~ /mingw|mswin/
     it "#socket should raise as it's not supported" do
-      lambda {
+      expect {
         @client.socket
-      }.should raise_error(Mysql2::Error)
+      }.to raise_error(Mysql2::Error)
     end
   end
 
   it "should respond to escape" do
-    Mysql2::Client.should respond_to(:escape)
+    expect(Mysql2::Client).to respond_to(:escape)
   end
 
   context "escape" do
     it "should return a new SQL-escape version of the passed string" do
-      Mysql2::Client.escape("abc'def\"ghi\0jkl%mno").should eql("abc\\'def\\\"ghi\\0jkl%mno")
+      expect(Mysql2::Client.escape("abc'def\"ghi\0jkl%mno")).to eql("abc\\'def\\\"ghi\\0jkl%mno")
     end
 
     it "should return the passed string if nothing was escaped" do
       str = "plain"
-      Mysql2::Client.escape(str).object_id.should eql(str.object_id)
+      expect(Mysql2::Client.escape(str).object_id).to eql(str.object_id)
     end
 
     it "should not overflow the thread stack" do
-      lambda {
+      expect {
         Thread.new { Mysql2::Client.escape("'" * 256 * 1024) }.join
-      }.should_not raise_error(SystemStackError)
+      }.not_to raise_error
     end
 
     it "should not overflow the process stack" do
-      lambda {
+      expect {
         Thread.new { Mysql2::Client.escape("'" * 1024 * 1024 * 4) }.join
-      }.should_not raise_error(SystemStackError)
+      }.not_to raise_error
     end
 
     unless RUBY_VERSION =~ /1.8/
       it "should carry over the original string's encoding" do
         str = "abc'def\"ghi\0jkl%mno"
         escaped = Mysql2::Client.escape(str)
-        escaped.encoding.should eql(str.encoding)
+        expect(escaped.encoding).to eql(str.encoding)
 
         str.encode!('us-ascii')
         escaped = Mysql2::Client.escape(str)
-        escaped.encoding.should eql(str.encoding)
+        expect(escaped.encoding).to eql(str.encoding)
       end
     end
   end
 
   it "should respond to #escape" do
-    @client.should respond_to(:escape)
+    expect(@client).to respond_to(:escape)
   end
 
   context "#escape" do
     it "should return a new SQL-escape version of the passed string" do
-      @client.escape("abc'def\"ghi\0jkl%mno").should eql("abc\\'def\\\"ghi\\0jkl%mno")
+      expect(@client.escape("abc'def\"ghi\0jkl%mno")).to eql("abc\\'def\\\"ghi\\0jkl%mno")
     end
 
     it "should return the passed string if nothing was escaped" do
       str = "plain"
-      @client.escape(str).object_id.should eql(str.object_id)
+      expect(@client.escape(str).object_id).to eql(str.object_id)
     end
 
     it "should not overflow the thread stack" do
-      lambda {
+      expect {
         Thread.new { @client.escape("'" * 256 * 1024) }.join
-      }.should_not raise_error(SystemStackError)
+      }.not_to raise_error
     end
 
     it "should not overflow the process stack" do
-      lambda {
+      expect {
         Thread.new { @client.escape("'" * 1024 * 1024 * 4) }.join
-      }.should_not raise_error(SystemStackError)
+      }.not_to raise_error
     end
 
     it "should require an open connection" do
       @client.close
-      lambda {
+      expect {
         @client.escape ""
-      }.should raise_error(Mysql2::Error)
+      }.to raise_error(Mysql2::Error)
+    end
+
+    context 'when mysql encoding is not utf8' do
+      before { pending('Encoding is undefined') unless defined?(Encoding) }
+
+      let(:client) { Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => "ujis")) }
+
+      it 'should return a internal encoding string if Encoding.default_internal is set' do
+        with_internal_encoding Encoding::UTF_8 do
+          expect(client.escape("\u{30C6}\u{30B9}\u{30C8}")).to eq "\u{30C6}\u{30B9}\u{30C8}"
+          expect(client.escape("\u{30C6}'\u{30B9}\"\u{30C8}")).to eq "\u{30C6}\\'\u{30B9}\\\"\u{30C8}"
+        end
+      end
     end
   end
 
   it "should respond to #info" do
-    @client.should respond_to(:info)
+    expect(@client).to respond_to(:info)
   end
 
   it "#info should return a hash containing the client version ID and String" do
     info = @client.info
-    info.class.should eql(Hash)
-    info.should have_key(:id)
-    info[:id].class.should eql(Fixnum)
-    info.should have_key(:version)
-    info[:version].class.should eql(String)
+    expect(info).to be_an_instance_of(Hash)
+    expect(info).to have_key(:id)
+    expect(info[:id]).to be_an_instance_of(Fixnum)
+    expect(info).to have_key(:version)
+    expect(info[:version]).to be_an_instance_of(String)
   end
 
-  if defined? Encoding
-    context "strings returned by #info" do
-      it "should default to the connection's encoding if Encoding.default_internal is nil" do
-        with_internal_encoding nil do
-          @client.info[:version].encoding.should eql(Encoding.find('utf-8'))
+  context "strings returned by #info" do
+    before { pending('Encoding is undefined') unless defined?(Encoding) }
 
-          client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii'))
-          client2.info[:version].encoding.should eql(Encoding.find('us-ascii'))
-        end
-      end
+    it "should be tagged as ascii" do
+      expect(@client.info[:version].encoding).to eql(Encoding::US_ASCII)
+      expect(@client.info[:header_version].encoding).to eql(Encoding::US_ASCII)
+    end
+  end
 
-      it "should use Encoding.default_internal" do
-        with_internal_encoding 'utf-8' do
-          @client.info[:version].encoding.should eql(Encoding.default_internal)
-        end
+  context "strings returned by .info" do
+    before { pending('Encoding is undefined') unless defined?(Encoding) }
 
-        with_internal_encoding 'us-ascii' do
-          @client.info[:version].encoding.should eql(Encoding.default_internal)
-        end
-      end
+    it "should be tagged as ascii" do
+      expect(Mysql2::Client.info[:version].encoding).to eql(Encoding::US_ASCII)
+      expect(Mysql2::Client.info[:header_version].encoding).to eql(Encoding::US_ASCII)
     end
   end
 
   it "should respond to #server_info" do
-    @client.should respond_to(:server_info)
+    expect(@client).to respond_to(:server_info)
   end
 
   it "#server_info should return a hash containing the client version ID and String" do
     server_info = @client.server_info
-    server_info.class.should eql(Hash)
-    server_info.should have_key(:id)
-    server_info[:id].class.should eql(Fixnum)
-    server_info.should have_key(:version)
-    server_info[:version].class.should eql(String)
+    expect(server_info).to be_an_instance_of(Hash)
+    expect(server_info).to have_key(:id)
+    expect(server_info[:id]).to be_an_instance_of(Fixnum)
+    expect(server_info).to have_key(:version)
+    expect(server_info[:version]).to be_an_instance_of(String)
   end
 
   it "#server_info should require an open connection" do
     @client.close
-    lambda {
+    expect {
       @client.server_info
-    }.should raise_error(Mysql2::Error)
+    }.to raise_error(Mysql2::Error)
   end
 
-  if defined? Encoding
-    context "strings returned by #server_info" do
-      it "should default to the connection's encoding if Encoding.default_internal is nil" do
-        with_internal_encoding nil do
-          @client.server_info[:version].encoding.should eql(Encoding.find('utf-8'))
+  context "strings returned by #server_info" do
+    before { pending('Encoding is undefined') unless defined?(Encoding) }
 
-          client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii'))
-          client2.server_info[:version].encoding.should eql(Encoding.find('us-ascii'))
-        end
+    it "should default to the connection's encoding if Encoding.default_internal is nil" do
+      with_internal_encoding nil do
+        expect(@client.server_info[:version].encoding).to eql(Encoding::UTF_8)
+
+        client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii'))
+        expect(client2.server_info[:version].encoding).to eql(Encoding::ASCII)
       end
+    end
 
-      it "should use Encoding.default_internal" do
-        with_internal_encoding 'utf-8' do
-          @client.server_info[:version].encoding.should eql(Encoding.default_internal)
-        end
+    it "should use Encoding.default_internal" do
+      with_internal_encoding Encoding::UTF_8 do
+        expect(@client.server_info[:version].encoding).to eql(Encoding.default_internal)
+      end
 
-        with_internal_encoding 'us-ascii' do
-          @client.server_info[:version].encoding.should eql(Encoding.default_internal)
-        end
+      with_internal_encoding Encoding::ASCII do
+        expect(@client.server_info[:version].encoding).to eql(Encoding.default_internal)
       end
     end
   end
 
   it "should raise a Mysql2::Error exception upon connection failure" do
-    lambda {
+    expect {
       Mysql2::Client.new :host => "localhost", :username => 'asdfasdf8d2h', :password => 'asdfasdfw42'
-    }.should raise_error(Mysql2::Error)
+    }.to raise_error(Mysql2::Error)
 
-    lambda {
+    expect {
       Mysql2::Client.new DatabaseCredentials['root']
-    }.should_not raise_error(Mysql2::Error)
+    }.not_to raise_error
   end
 
   context 'write operations api' do
@@ -802,46 +844,46 @@ describe Mysql2::Client do
     end
 
     it "should respond to #last_id" do
-      @client.should respond_to(:last_id)
+      expect(@client).to respond_to(:last_id)
     end
 
     it "#last_id should return a Fixnum, the from the last INSERT/UPDATE" do
-      @client.last_id.should eql(0)
+      expect(@client.last_id).to eql(0)
       @client.query "INSERT INTO lastIdTest (blah) VALUES (1234)"
-      @client.last_id.should eql(1)
+      expect(@client.last_id).to eql(1)
     end
 
     it "should respond to #last_id" do
-      @client.should respond_to(:last_id)
+      expect(@client).to respond_to(:last_id)
     end
 
     it "#last_id should return a Fixnum, the from the last INSERT/UPDATE" do
       @client.query "INSERT INTO lastIdTest (blah) VALUES (1234)"
-      @client.affected_rows.should eql(1)
+      expect(@client.affected_rows).to eql(1)
       @client.query "UPDATE lastIdTest SET blah=4321 WHERE id=1"
-      @client.affected_rows.should eql(1)
+      expect(@client.affected_rows).to eql(1)
     end
 
     it "#last_id should handle BIGINT auto-increment ids above 32 bits" do
       # The id column type must be BIGINT. Surprise: INT(x) is limited to 32-bits for all values of x.
       # Insert a row with a given ID, this should raise the auto-increment state
       @client.query "INSERT INTO lastIdTest (id, blah) VALUES (5000000000, 5000)"
-      @client.last_id.should eql(5000000000)
+      expect(@client.last_id).to eql(5000000000)
       @client.query "INSERT INTO lastIdTest (blah) VALUES (5001)"
-      @client.last_id.should eql(5000000001)
+      expect(@client.last_id).to eql(5000000001)
     end
   end
 
   it "should respond to #thread_id" do
-    @client.should respond_to(:thread_id)
+    expect(@client).to respond_to(:thread_id)
   end
 
   it "#thread_id should be a Fixnum" do
-    @client.thread_id.class.should eql(Fixnum)
+    expect(@client.thread_id).to be_an_instance_of(Fixnum)
   end
 
   it "should respond to #ping" do
-    @client.should respond_to(:ping)
+    expect(@client).to respond_to(:ping)
   end
 
   context "select_db" do
@@ -860,38 +902,38 @@ describe Mysql2::Client do
     end
 
     it "should respond to #select_db" do
-      @client.should respond_to(:select_db)
+      expect(@client).to respond_to(:select_db)
     end
 
     it "should switch databases" do
       @client.select_db("test_selectdb_0")
-      @client.query("SHOW TABLES").first.values.first.should eql("test0")
+      expect(@client.query("SHOW TABLES").first.values.first).to eql("test0")
       @client.select_db("test_selectdb_1")
-      @client.query("SHOW TABLES").first.values.first.should eql("test1")
+      expect(@client.query("SHOW TABLES").first.values.first).to eql("test1")
       @client.select_db("test_selectdb_0")
-      @client.query("SHOW TABLES").first.values.first.should eql("test0")
+      expect(@client.query("SHOW TABLES").first.values.first).to eql("test0")
     end
 
     it "should raise a Mysql2::Error when the database doesn't exist" do
-      lambda {
+      expect {
         @client.select_db("nopenothere")
-      }.should raise_error(Mysql2::Error)
+      }.to raise_error(Mysql2::Error)
     end
 
     it "should return the database switched to" do
-      @client.select_db("test_selectdb_1").should eq("test_selectdb_1")
+      expect(@client.select_db("test_selectdb_1")).to eq("test_selectdb_1")
     end
   end
 
   it "#thread_id should return a boolean" do
-    @client.ping.should eql(true)
+    expect(@client.ping).to eql(true)
     @client.close
-    @client.ping.should eql(false)
+    expect(@client.ping).to eql(false)
   end
 
   unless RUBY_VERSION =~ /1.8/
     it "should respond to #encoding" do
-      @client.should respond_to(:encoding)
+      expect(@client).to respond_to(:encoding)
     end
   end
 end
diff --git a/spec/mysql2/error_spec.rb b/spec/mysql2/error_spec.rb
index 8b5cb2b..ea4af54 100644
--- a/spec/mysql2/error_spec.rb
+++ b/spec/mysql2/error_spec.rb
@@ -2,81 +2,82 @@
 
 require 'spec_helper'
 
-describe Mysql2::Error do
+RSpec.describe Mysql2::Error do
   let(:client) { Mysql2::Client.new(DatabaseCredentials['root']) }
 
-  let :error do
+  let(:error) do
     begin
       client.query("HAHAHA")
     rescue Mysql2::Error => e
       error = e
-    ensure
-      client.close
     end
 
     error
   end
 
   it "responds to error_number and sql_state, with aliases" do
-    error.should respond_to(:error_number)
-    error.should respond_to(:sql_state)
+    expect(error).to respond_to(:error_number)
+    expect(error).to respond_to(:sql_state)
 
     # Mysql gem compatibility
-    error.should respond_to(:errno)
-    error.should respond_to(:error)
+    expect(error).to respond_to(:errno)
+    expect(error).to respond_to(:error)
   end
 
-  if "".respond_to? :encoding
-    let :error do
-      client = Mysql2::Client.new(DatabaseCredentials['root'])
+  context 'encoding' do
+    let(:valid_utf8) { '造字' }
+    let(:error) do
       begin
-        client.query("\xE9\x80\xA0\xE5\xAD\x97")
+        client.query(valid_utf8)
       rescue Mysql2::Error => e
-        error = e
-      ensure
-        client.close
+        e
       end
-
-      error
     end
 
-    let :bad_err do
-      client = Mysql2::Client.new(DatabaseCredentials['root'])
+    let(:invalid_utf8) { "\xE5\xC6\x7D\x1F" }
+    let(:bad_err) do
       begin
-        client.query("\xE5\xC6\x7D\x1F")
+        client.query(invalid_utf8)
       rescue Mysql2::Error => e
-        error = e
-      ensure
-        client.close
+        e
       end
+    end
+
+    before do
+      pending('String#encoding is not defined') unless String.public_method_defined?(:encoding)
+
+      # sanity check
+      expect(valid_utf8.encoding).to eql(Encoding::UTF_8)
+      expect(valid_utf8).to be_valid_encoding
 
-      error
+      expect(invalid_utf8.encoding).to eql(Encoding::UTF_8)
+      expect(invalid_utf8).to_not be_valid_encoding
     end
 
     it "returns error messages as UTF-8 by default" do
       with_internal_encoding nil do
-        error.message.encoding.should eql(Encoding::UTF_8)
-        error.message.valid_encoding?
+        expect(error.message.encoding).to eql(Encoding::UTF_8)
+        expect(error.message).to be_valid_encoding
 
-        bad_err.message.encoding.should eql(Encoding::UTF_8)
-        bad_err.message.valid_encoding?
+        expect(bad_err.message.encoding).to eql(Encoding::UTF_8)
+        expect(bad_err.message).to be_valid_encoding
 
-        bad_err.message.should include("??}\u001F")
+        expect(bad_err.message).to include("??}\u001F")
       end
     end
 
     it "returns sql state as ASCII" do
-      error.sql_state.encoding.should eql(Encoding::US_ASCII)
-      error.sql_state.valid_encoding?
+      expect(error.sql_state.encoding).to eql(Encoding::US_ASCII)
+      expect(error.sql_state).to be_valid_encoding
     end
 
     it "returns error messages and sql state in Encoding.default_internal if set" do
-      with_internal_encoding 'UTF-16LE' do
-        error.message.encoding.should eql(Encoding.default_internal)
-        error.message.valid_encoding?
+      with_internal_encoding Encoding::UTF_16LE do
+        expect(error.message.encoding).to eql(Encoding.default_internal)
+        expect(error.message).to be_valid_encoding
 
-        bad_err.message.encoding.should eql(Encoding.default_internal)
-        bad_err.message.valid_encoding?
+        expect(bad_err.message.encoding).to eql(Encoding.default_internal)
+        expect(bad_err.message).to be_valid_encoding
       end
     end
   end
diff --git a/spec/mysql2/result_spec.rb b/spec/mysql2/result_spec.rb
index 964bc43..6844c88 100644
--- a/spec/mysql2/result_spec.rb
+++ b/spec/mysql2/result_spec.rb
@@ -1,39 +1,47 @@
 # encoding: UTF-8
 require 'spec_helper'
 
-describe Mysql2::Result do
+RSpec.describe Mysql2::Result do
   before(:each) do
     @result = @client.query "SELECT 1"
   end
 
+  it "should raise a TypeError exception when it doesn't wrap a result set" do
+    r = Mysql2::Result.new
+    expect { r.count }.to raise_error(TypeError)
+    expect { r.fields }.to raise_error(TypeError)
+    expect { r.size }.to raise_error(TypeError)
+    expect { r.each }.to raise_error(TypeError)
+  end
+
   it "should have included Enumerable" do
-    Mysql2::Result.ancestors.include?(Enumerable).should be_true
+    expect(Mysql2::Result.ancestors.include?(Enumerable)).to be true
   end
 
   it "should respond to #each" do
-    @result.should respond_to(:each)
+    expect(@result).to respond_to(:each)
   end
 
   it "should raise a Mysql2::Error exception upon a bad query" do
-    lambda {
+    expect {
       @client.query "bad sql"
-    }.should raise_error(Mysql2::Error)
+    }.to raise_error(Mysql2::Error)
 
-    lambda {
+    expect {
       @client.query "SELECT 1"
-    }.should_not raise_error(Mysql2::Error)
+    }.not_to raise_error
   end
 
   it "should respond to #count, which is aliased as #size" do
     r = @client.query "SELECT 1"
-    r.should respond_to :count
-    r.should respond_to :size
+    expect(r).to respond_to :count
+    expect(r).to respond_to :size
   end
 
   it "should be able to return the number of rows in the result set" do
     r = @client.query "SELECT 1"
-    r.count.should eql(1)
-    r.size.should eql(1)
+    expect(r.count).to eql(1)
+    expect(r.size).to eql(1)
   end
 
   context "metadata queries" do
@@ -45,39 +53,39 @@ describe Mysql2::Result do
   context "#each" do
     it "should yield rows as hash's" do
       @result.each do |row|
-        row.class.should eql(Hash)
+        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
       @result.each(:symbolize_keys => true) do |row|
-        row.keys.first.class.should eql(Symbol)
+        expect(row.keys.first).to be_an_instance_of(Symbol)
       end
     end
 
     it "should be able to return results as an array" do
       @result.each(:as => :array) do |row|
-        row.class.should eql(Array)
+        expect(row).to be_an_instance_of(Array)
       end
     end
 
     it "should cache previously yielded results by default" do
-      @result.first.object_id.should eql(@result.first.object_id)
+      expect(@result.first.object_id).to 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)
+      expect(result.first.object_id).not_to eql(result.first.object_id)
     end
 
     it "should yield different value for #first if streaming" do
       result = @client.query "SELECT 1 UNION SELECT 2", :stream => true, :cache_rows => false
-      result.first.should_not eql(result.first)
+      expect(result.first).not_to eql(result.first)
     end
 
     it "should yield the same value for #first if streaming is disabled" do
       result = @client.query "SELECT 1 UNION SELECT 2", :stream => false
-      result.first.should eql(result.first)
+      expect(result.first).to eql(result.first)
     end
 
     it "should throw an exception if we try to iterate twice when streaming is enabled" do
@@ -96,41 +104,43 @@ describe Mysql2::Result do
     end
 
     it "method should exist" do
-      @test_result.should respond_to(:fields)
+      expect(@test_result).to respond_to(:fields)
     end
 
     it "should return an array of field names in proper order" do
       result = @client.query "SELECT 'a', 'b', 'c'"
-      result.fields.should eql(['a', 'b', 'c'])
+      expect(result.fields).to eql(%w(a b c))
     end
   end
 
   context "streaming" do
     it "should maintain a count while streaming" do
-      result = @client.query('SELECT 1')
-
-      result.count.should eql(1)
+      result = @client.query('SELECT 1', :stream => true, :cache_rows => false)
+      expect(result.count).to eql(0)
       result.each.to_a
-      result.count.should eql(1)
+      expect(result.count).to eql(1)
     end
 
-    it "should set the actual count of rows after streaming" do
-      result = @client.query("SELECT * FROM mysql2_test", :stream => true, :cache_rows => false)
-      result.count.should eql(0)
-      result.each {|r|  }
-      result.count.should eql(1)
+    it "should retain the count when mixing first and each" do
+      result = @client.query("SELECT 1 UNION SELECT 2", :stream => true, :cache_rows => false)
+      expect(result.count).to eql(0)
+      result.first
+      expect(result.count).to eql(1)
+      result.each.to_a
+      expect(result.count).to eql(2)
     end
 
     it "should not yield nil at the end of streaming" do
       result = @client.query('SELECT * FROM mysql2_test', :stream => true, :cache_rows => false)
-      result.each { |r| r.should_not be_nil}
+      result.each { |r| expect(r).not_to be_nil }
     end
 
     it "#count should be zero for rows after streaming when there were no results" do
+      @client.query "USE test"
       result = @client.query("SELECT * FROM mysql2_test WHERE null_test IS NOT NULL", :stream => true, :cache_rows => false)
-      result.count.should eql(0)
+      expect(result.count).to eql(0)
       result.each.to_a
-      result.count.should eql(0)
+      expect(result.count).to eql(0)
     end
 
     it "should raise an exception if streaming ended due to a timeout" do
@@ -147,12 +157,12 @@ describe Mysql2::Result do
       client.query "SET net_write_timeout = 1"
       res = client.query "SELECT * FROM streamingTest", :stream => true, :cache_rows => false
 
-      lambda {
-        res.each_with_index do |row, i|
+      expect {
+        res.each_with_index do |_, i|
           # Exhaust the first result packet then trigger a timeout
           sleep 2 if i > 0 && i % 1000 == 0
         end
-      }.should raise_error(Mysql2::Error, /Lost connection/)
+      }.to raise_error(Mysql2::Error, /Lost connection/)
     end
   end
 
@@ -163,32 +173,32 @@ describe Mysql2::Result do
 
     it "should return nil values for NULL and strings for everything else when :cast is false" do
       result = @client.query('SELECT null_test, tiny_int_test, bool_cast_test, int_test, date_test, enum_test FROM mysql2_test WHERE bool_cast_test = 1 LIMIT 1', :cast => false).first
-      result["null_test"].should be_nil
-      result["tiny_int_test"].should  eql("1")
-      result["bool_cast_test"].should eql("1")
-      result["int_test"].should       eql("10")
-      result["date_test"].should      eql("2010-04-04")
-      result["enum_test"].should      eql("val1")
+      expect(result["null_test"]).to be_nil
+      expect(result["tiny_int_test"]).to eql("1")
+      expect(result["bool_cast_test"]).to eql("1")
+      expect(result["int_test"]).to eql("10")
+      expect(result["date_test"]).to eql("2010-04-04")
+      expect(result["enum_test"]).to eql("val1")
     end
 
     it "should return nil for a NULL value" do
-      @test_result['null_test'].class.should eql(NilClass)
-      @test_result['null_test'].should eql(nil)
+      expect(@test_result['null_test']).to be_an_instance_of(NilClass)
+      expect(@test_result['null_test']).to eql(nil)
     end
 
     it "should return String for a BIT(64) value" do
-      @test_result['bit_test'].class.should eql(String)
-      @test_result['bit_test'].should eql("\000\000\000\000\000\000\000\005")
+      expect(@test_result['bit_test']).to be_an_instance_of(String)
+      expect(@test_result['bit_test']).to eql("\000\000\000\000\000\000\000\005")
     end
 
     it "should return String for a BIT(1) value" do
-      @test_result['single_bit_test'].class.should eql(String)
-      @test_result['single_bit_test'].should eql("\001")
+      expect(@test_result['single_bit_test']).to be_an_instance_of(String)
+      expect(@test_result['single_bit_test']).to eql("\001")
     end
 
     it "should return Fixnum for a TINYINT value" do
-      [Fixnum, Bignum].should include(@test_result['tiny_int_test'].class)
-      @test_result['tiny_int_test'].should eql(1)
+      expect([Fixnum, Bignum]).to include(@test_result['tiny_int_test'].class)
+      expect(@test_result['tiny_int_test']).to eql(1)
     end
 
     it "should return TrueClass or FalseClass for a TINYINT value if :cast_booleans is enabled" do
@@ -202,9 +212,9 @@ describe Mysql2::Result do
       result1 = @client.query 'SELECT bool_cast_test FROM mysql2_test WHERE bool_cast_test = 1 LIMIT 1', :cast_booleans => true
       result2 = @client.query 'SELECT bool_cast_test FROM mysql2_test WHERE bool_cast_test = 0 LIMIT 1', :cast_booleans => true
       result3 = @client.query 'SELECT bool_cast_test FROM mysql2_test WHERE bool_cast_test = -1 LIMIT 1', :cast_booleans => true
-      result1.first['bool_cast_test'].should be_true
-      result2.first['bool_cast_test'].should be_false
-      result3.first['bool_cast_test'].should be_true
+      expect(result1.first['bool_cast_test']).to be true
+      expect(result2.first['bool_cast_test']).to be false
+      expect(result3.first['bool_cast_test']).to be true
 
       @client.query "DELETE from mysql2_test WHERE id IN(#{id1},#{id2},#{id3})"
     end
@@ -217,219 +227,214 @@ describe Mysql2::Result do
 
       result1 = @client.query "SELECT single_bit_test FROM mysql2_test WHERE id = #{id1}", :cast_booleans => true
       result2 = @client.query "SELECT single_bit_test FROM mysql2_test WHERE id = #{id2}", :cast_booleans => true
-      result1.first['single_bit_test'].should be_true
-      result2.first['single_bit_test'].should be_false
+      expect(result1.first['single_bit_test']).to be true
+      expect(result2.first['single_bit_test']).to be false
 
       @client.query "DELETE from mysql2_test WHERE id IN(#{id1},#{id2})"
     end
 
     it "should return Fixnum for a SMALLINT value" do
-      [Fixnum, Bignum].should include(@test_result['small_int_test'].class)
-      @test_result['small_int_test'].should eql(10)
+      expect([Fixnum, Bignum]).to include(@test_result['small_int_test'].class)
+      expect(@test_result['small_int_test']).to eql(10)
     end
 
     it "should return Fixnum for a MEDIUMINT value" do
-      [Fixnum, Bignum].should include(@test_result['medium_int_test'].class)
-      @test_result['medium_int_test'].should eql(10)
+      expect([Fixnum, Bignum]).to include(@test_result['medium_int_test'].class)
+      expect(@test_result['medium_int_test']).to eql(10)
     end
 
     it "should return Fixnum for an INT value" do
-      [Fixnum, Bignum].should include(@test_result['int_test'].class)
-      @test_result['int_test'].should eql(10)
+      expect([Fixnum, Bignum]).to include(@test_result['int_test'].class)
+      expect(@test_result['int_test']).to eql(10)
     end
 
     it "should return Fixnum for a BIGINT value" do
-      [Fixnum, Bignum].should include(@test_result['big_int_test'].class)
-      @test_result['big_int_test'].should eql(10)
+      expect([Fixnum, Bignum]).to include(@test_result['big_int_test'].class)
+      expect(@test_result['big_int_test']).to eql(10)
     end
 
     it "should return Fixnum for a YEAR value" do
-      [Fixnum, Bignum].should include(@test_result['year_test'].class)
-      @test_result['year_test'].should eql(2009)
+      expect([Fixnum, Bignum]).to include(@test_result['year_test'].class)
+      expect(@test_result['year_test']).to eql(2009)
     end
 
     it "should return BigDecimal for a DECIMAL value" do
-      @test_result['decimal_test'].class.should eql(BigDecimal)
-      @test_result['decimal_test'].should eql(10.3)
+      expect(@test_result['decimal_test']).to be_an_instance_of(BigDecimal)
+      expect(@test_result['decimal_test']).to eql(10.3)
     end
 
     it "should return Float for a FLOAT value" do
-      @test_result['float_test'].class.should eql(Float)
-      @test_result['float_test'].should eql(10.3)
+      expect(@test_result['float_test']).to be_an_instance_of(Float)
+      expect(@test_result['float_test']).to eql(10.3)
     end
 
     it "should return Float for a DOUBLE value" do
-      @test_result['double_test'].class.should eql(Float)
-      @test_result['double_test'].should eql(10.3)
+      expect(@test_result['double_test']).to be_an_instance_of(Float)
+      expect(@test_result['double_test']).to eql(10.3)
     end
 
     it "should return Time for a DATETIME value when within the supported range" do
-      @test_result['date_time_test'].class.should eql(Time)
-      @test_result['date_time_test'].strftime("%Y-%m-%d %H:%M:%S").should eql('2010-04-04 11:44:00')
+      expect(@test_result['date_time_test']).to be_an_instance_of(Time)
+      expect(@test_result['date_time_test'].strftime("%Y-%m-%d %H:%M:%S")).to eql('2010-04-04 11:44:00')
     end
 
     if 1.size == 4 # 32bit
-      unless RUBY_VERSION =~ /1.8/
-        klass = Time
+      klass = if RUBY_VERSION =~ /1.8/
+        DateTime
       else
-        klass = DateTime
+        Time
       end
 
       it "should return DateTime when timestamp is < 1901-12-13 20:45:52" do
-                                      # 1901-12-13T20:45:52 is the min for 32bit Ruby 1.8
+        # 1901-12-13T20:45:52 is the min for 32bit Ruby 1.8
         r = @client.query("SELECT CAST('1901-12-13 20:45:51' AS DATETIME) as test")
-        r.first['test'].class.should eql(klass)
+        expect(r.first['test']).to be_an_instance_of(klass)
       end
 
       it "should return DateTime when timestamp is > 2038-01-19T03:14:07" do
-                                      # 2038-01-19T03:14:07 is the max for 32bit Ruby 1.8
+        # 2038-01-19T03:14:07 is the max for 32bit Ruby 1.8
         r = @client.query("SELECT CAST('2038-01-19 03:14:08' AS DATETIME) as test")
-        r.first['test'].class.should eql(klass)
+        expect(r.first['test']).to be_an_instance_of(klass)
       end
     elsif 1.size == 8 # 64bit
-      unless RUBY_VERSION =~ /1.8/
-        it "should return Time when timestamp is < 1901-12-13 20:45:52" do
-          r = @client.query("SELECT CAST('1901-12-13 20:45:51' AS DATETIME) as test")
-          r.first['test'].class.should eql(Time)
-        end
-
-        it "should return Time when timestamp is > 2038-01-19T03:14:07" do
-          r = @client.query("SELECT CAST('2038-01-19 03:14:08' AS DATETIME) as test")
-          r.first['test'].class.should eql(Time)
-        end
-      else
+      if RUBY_VERSION =~ /1.8/
         it "should return Time when timestamp is > 0138-12-31 11:59:59" do
           r = @client.query("SELECT CAST('0139-1-1 00:00:00' AS DATETIME) as test")
-          r.first['test'].class.should eql(Time)
+          expect(r.first['test']).to be_an_instance_of(Time)
         end
 
         it "should return DateTime when timestamp is < 0139-1-1T00:00:00" do
           r = @client.query("SELECT CAST('0138-12-31 11:59:59' AS DATETIME) as test")
-          r.first['test'].class.should eql(DateTime)
+          expect(r.first['test']).to be_an_instance_of(DateTime)
+        end
+
+        it "should return Time when timestamp is > 2038-01-19T03:14:07" do
+          r = @client.query("SELECT CAST('2038-01-19 03:14:08' AS DATETIME) as test")
+          expect(r.first['test']).to be_an_instance_of(Time)
+        end
+      else
+        it "should return Time when timestamp is < 1901-12-13 20:45:52" do
+          r = @client.query("SELECT CAST('1901-12-13 20:45:51' AS DATETIME) as test")
+          expect(r.first['test']).to be_an_instance_of(Time)
         end
 
         it "should return Time when timestamp is > 2038-01-19T03:14:07" do
           r = @client.query("SELECT CAST('2038-01-19 03:14:08' AS DATETIME) as test")
-          r.first['test'].class.should eql(Time)
+          expect(r.first['test']).to be_an_instance_of(Time)
         end
       end
     end
 
     it "should return Time for a TIMESTAMP value when within the supported range" do
-      @test_result['timestamp_test'].class.should eql(Time)
-      @test_result['timestamp_test'].strftime("%Y-%m-%d %H:%M:%S").should eql('2010-04-04 11:44:00')
+      expect(@test_result['timestamp_test']).to be_an_instance_of(Time)
+      expect(@test_result['timestamp_test'].strftime("%Y-%m-%d %H:%M:%S")).to eql('2010-04-04 11:44:00')
     end
 
     it "should return Time for a TIME value" do
-      @test_result['time_test'].class.should eql(Time)
-      @test_result['time_test'].strftime("%Y-%m-%d %H:%M:%S").should eql('2000-01-01 11:44:00')
+      expect(@test_result['time_test']).to be_an_instance_of(Time)
+      expect(@test_result['time_test'].strftime("%Y-%m-%d %H:%M:%S")).to eql('2000-01-01 11:44:00')
     end
 
     it "should return Date for a DATE value" do
-      @test_result['date_test'].class.should eql(Date)
-      @test_result['date_test'].strftime("%Y-%m-%d").should eql('2010-04-04')
+      expect(@test_result['date_test']).to be_an_instance_of(Date)
+      expect(@test_result['date_test'].strftime("%Y-%m-%d")).to eql('2010-04-04')
     end
 
     it "should return String for an ENUM value" do
-      @test_result['enum_test'].class.should eql(String)
-      @test_result['enum_test'].should eql('val1')
+      expect(@test_result['enum_test']).to be_an_instance_of(String)
+      expect(@test_result['enum_test']).to eql('val1')
     end
 
     it "should raise an error given an invalid DATETIME" do
-      begin
-        @client.query("SELECT CAST('1972-00-27 00:00:00' AS DATETIME) as bad_datetime").each
-      rescue Mysql2::Error => e
-        error = e
-      end
-
-      error.message.should eql("Invalid date in field 'bad_datetime': 1972-00-27 00:00:00")
+      expect { @client.query("SELECT CAST('1972-00-27 00:00:00' AS DATETIME) as bad_datetime").each }.to \
+        raise_error(Mysql2::Error, "Invalid date in field 'bad_datetime': 1972-00-27 00:00:00")
     end
 
-    if defined? Encoding
-      context "string encoding for ENUM values" do
-        it "should default to the connection's encoding if Encoding.default_internal is nil" do
-          with_internal_encoding nil do
-            result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
-            result['enum_test'].encoding.should eql(Encoding.find('utf-8'))
+    context "string encoding for ENUM values" do
+      before { pending('Encoding is undefined') unless defined?(Encoding) }
 
-            client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii'))
-            result = client2.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
-            result['enum_test'].encoding.should eql(Encoding.find('us-ascii'))
-            client2.close
-          end
+      it "should default to the connection's encoding if Encoding.default_internal is nil" do
+        with_internal_encoding nil do
+          result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['enum_test'].encoding).to eql(Encoding::UTF_8)
+
+          client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii'))
+          result = client2.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['enum_test'].encoding).to eql(Encoding::ASCII)
+          client2.close
         end
+      end
 
-        it "should use Encoding.default_internal" do
-          with_internal_encoding 'utf-8' do
-            result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
-            result['enum_test'].encoding.should eql(Encoding.default_internal)
-          end
+      it "should use Encoding.default_internal" do
+        with_internal_encoding Encoding::UTF_8 do
+          result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['enum_test'].encoding).to eql(Encoding.default_internal)
+        end
 
-          with_internal_encoding 'us-ascii' do
-            result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
-            result['enum_test'].encoding.should eql(Encoding.default_internal)
-          end
+        with_internal_encoding Encoding::ASCII do
+          result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['enum_test'].encoding).to eql(Encoding.default_internal)
         end
       end
     end
 
     it "should return String for a SET value" do
-      @test_result['set_test'].class.should eql(String)
-      @test_result['set_test'].should eql('val1,val2')
+      expect(@test_result['set_test']).to be_an_instance_of(String)
+      expect(@test_result['set_test']).to eql('val1,val2')
     end
 
-    if defined? Encoding
-      context "string encoding for SET values" do
-        it "should default to the connection's encoding if Encoding.default_internal is nil" do
-          with_internal_encoding nil do
-            result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
-            result['set_test'].encoding.should eql(Encoding.find('utf-8'))
+    context "string encoding for SET values" do
+      before { pending('Encoding is undefined') unless defined?(Encoding) }
 
-            client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii'))
-            result = client2.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
-            result['set_test'].encoding.should eql(Encoding.find('us-ascii'))
-            client2.close
-          end
+      it "should default to the connection's encoding if Encoding.default_internal is nil" do
+        with_internal_encoding nil do
+          result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['set_test'].encoding).to eql(Encoding::UTF_8)
+
+          client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii'))
+          result = client2.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['set_test'].encoding).to eql(Encoding::ASCII)
+          client2.close
         end
+      end
 
-        it "should use Encoding.default_internal" do
-          with_internal_encoding 'utf-8' do
-            result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
-            result['set_test'].encoding.should eql(Encoding.default_internal)
-          end
+      it "should use Encoding.default_internal" do
+        with_internal_encoding Encoding::UTF_8 do
+          result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['set_test'].encoding).to eql(Encoding.default_internal)
+        end
 
-          with_internal_encoding 'us-ascii' do
-            result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
-            result['set_test'].encoding.should eql(Encoding.default_internal)
-          end
+        with_internal_encoding Encoding::ASCII do
+          result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['set_test'].encoding).to eql(Encoding.default_internal)
         end
       end
     end
 
     it "should return String for a BINARY value" do
-      @test_result['binary_test'].class.should eql(String)
-      @test_result['binary_test'].should eql("test#{"\000"*6}")
+      expect(@test_result['binary_test']).to be_an_instance_of(String)
+      expect(@test_result['binary_test']).to eql("test#{"\000" * 6}")
     end
 
-    if defined? Encoding
-      context "string encoding for BINARY values" do
-        it "should default to binary if Encoding.default_internal is nil" do
-          with_internal_encoding nil do
-            result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
-            result['binary_test'].encoding.should eql(Encoding.find('binary'))
-          end
+    context "string encoding for BINARY values" do
+      before { pending('Encoding is undefined') unless defined?(Encoding) }
+
+      it "should default to binary if Encoding.default_internal is nil" do
+        with_internal_encoding nil do
+          result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['binary_test'].encoding).to eql(Encoding::BINARY)
         end
+      end
 
-        it "should not use Encoding.default_internal" do
-          with_internal_encoding 'utf-8' do
-            result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
-            result['binary_test'].encoding.should eql(Encoding.find('binary'))
-          end
+      it "should not use Encoding.default_internal" do
+        with_internal_encoding Encoding::UTF_8 do
+          result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['binary_test'].encoding).to eql(Encoding::BINARY)
+        end
 
-          with_internal_encoding 'us-ascii' do
-            result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
-            result['binary_test'].encoding.should eql(Encoding.find('binary'))
-          end
+        with_internal_encoding Encoding::ASCII do
+          result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['binary_test'].encoding).to eql(Encoding::BINARY)
         end
       end
     end
@@ -445,57 +450,57 @@ describe Mysql2::Result do
       'medium_blob_test' => 'MEDIUMBLOB',
       'medium_text_test' => 'MEDIUMTEXT',
       'long_blob_test' => 'LONGBLOB',
-      'long_text_test' => 'LONGTEXT'
+      'long_text_test' => 'LONGTEXT',
     }.each do |field, type|
       it "should return a String for #{type}" do
-        @test_result[field].class.should eql(String)
-        @test_result[field].should eql("test")
+        expect(@test_result[field]).to be_an_instance_of(String)
+        expect(@test_result[field]).to eql("test")
       end
 
-      if defined? Encoding
-        context "string encoding for #{type} values" do
-          if ['VARBINARY', 'TINYBLOB', 'BLOB', 'MEDIUMBLOB', 'LONGBLOB'].include?(type)
-            it "should default to binary if Encoding.default_internal is nil" do
-              with_internal_encoding nil do
-                result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
-                result['binary_test'].encoding.should eql(Encoding.find('binary'))
-              end
+      context "string encoding for #{type} values" do
+        before { pending('Encoding is undefined') unless defined?(Encoding) }
+
+        if %w(VARBINARY TINYBLOB BLOB MEDIUMBLOB LONGBLOB).include?(type)
+          it "should default to binary if Encoding.default_internal is nil" do
+            with_internal_encoding nil do
+              result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+              expect(result['binary_test'].encoding).to eql(Encoding::BINARY)
             end
+          end
 
-            it "should not use Encoding.default_internal" do
-              with_internal_encoding 'utf-8' do
-                result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
-                result['binary_test'].encoding.should eql(Encoding.find('binary'))
-              end
+          it "should not use Encoding.default_internal" do
+            with_internal_encoding Encoding::UTF_8 do
+              result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+              expect(result['binary_test'].encoding).to eql(Encoding::BINARY)
+            end
 
-              with_internal_encoding 'us-ascii' do
-                result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
-                result['binary_test'].encoding.should eql(Encoding.find('binary'))
-              end
+            with_internal_encoding Encoding::ASCII do
+              result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+              expect(result['binary_test'].encoding).to eql(Encoding::BINARY)
             end
-          else
-            it "should default to utf-8 if Encoding.default_internal is nil" do
-              with_internal_encoding nil do
-                result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
-                result[field].encoding.should eql(Encoding.find('utf-8'))
-
-                client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii'))
-                result = client2.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
-                result[field].encoding.should eql(Encoding.find('us-ascii'))
-                client2.close
-              end
+          end
+        else
+          it "should default to utf-8 if Encoding.default_internal is nil" do
+            with_internal_encoding nil do
+              result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+              expect(result[field].encoding).to eql(Encoding::UTF_8)
+
+              client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii'))
+              result = client2.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+              expect(result[field].encoding).to eql(Encoding::ASCII)
+              client2.close
             end
+          end
 
-            it "should use Encoding.default_internal" do
-              with_internal_encoding 'utf-8' do
-                result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
-                result[field].encoding.should eql(Encoding.default_internal)
-              end
+          it "should use Encoding.default_internal" do
+            with_internal_encoding Encoding::UTF_8 do
+              result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+              expect(result[field].encoding).to eql(Encoding.default_internal)
+            end
 
-              with_internal_encoding 'us-ascii' do
-                result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
-                result[field].encoding.should eql(Encoding.default_internal)
-              end
+            with_internal_encoding Encoding::ASCII do
+              result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+              expect(result[field].encoding).to eql(Encoding.default_internal)
             end
           end
         end
diff --git a/spec/mysql2/statement_spec.rb b/spec/mysql2/statement_spec.rb
new file mode 100644
index 0000000..69ede67
--- /dev/null
+++ b/spec/mysql2/statement_spec.rb
@@ -0,0 +1,684 @@
+# encoding: UTF-8
+require './spec/spec_helper.rb'
+
+RSpec.describe Mysql2::Statement do
+  before :each do
+    @client = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => "utf8"))
+  end
+
+  it "should create a statement" do
+    statement = nil
+    expect { statement = @client.prepare 'SELECT 1' }.to change {
+      @client.query("SHOW STATUS LIKE 'Prepared_stmt_count'").first['Value'].to_i
+    }.by(1)
+    expect(statement).to be_an_instance_of(Mysql2::Statement)
+  end
+
+  it "should raise an exception when server disconnects" do
+    @client.close
+    expect { @client.prepare 'SELECT 1' }.to raise_error(Mysql2::Error)
+  end
+
+  it "should tell us the param count" do
+    statement = @client.prepare 'SELECT ?, ?'
+    expect(statement.param_count).to eq(2)
+
+    statement2 = @client.prepare 'SELECT 1'
+    expect(statement2.param_count).to eq(0)
+  end
+
+  it "should tell us the field count" do
+    statement = @client.prepare 'SELECT ?, ?'
+    expect(statement.field_count).to eq(2)
+
+    statement2 = @client.prepare 'SELECT 1'
+    expect(statement2.field_count).to eq(1)
+  end
+
+  it "should let us execute our statement" do
+    statement = @client.prepare 'SELECT 1'
+    expect(statement.execute).not_to eq(nil)
+  end
+
+  it "should raise an exception without a block" do
+    statement = @client.prepare 'SELECT 1'
+    expect { statement.execute.each }.to raise_error(LocalJumpError)
+  end
+
+  it "should tell us the result count" do
+    statement = @client.prepare 'SELECT 1'
+    result = statement.execute
+    expect(result.count).to eq(1)
+  end
+
+  it "should let us iterate over results" do
+    statement = @client.prepare 'SELECT 1'
+    result = statement.execute
+    rows = []
+    result.each { |r| rows << r }
+    expect(rows).to eq([{ "1" => 1 }])
+  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)'
+    @client.query 'INSERT INTO mysql2_stmt_q (a) VALUES (1), (2)'
+    stmt = @client.prepare('SELECT a FROM mysql2_stmt_q WHERE a = ?')
+    result1 = stmt.execute(1)
+    result2 = stmt.execute(2)
+    expect(result2.first).to eq("a" => 2)
+    expect(result1.first).to eq("a" => 1)
+    @client.query 'DROP TABLE IF EXISTS mysql2_stmt_q'
+  end
+
+  it "should be reusable 1000 times" do
+    statement = @client.prepare 'SELECT 1'
+    1000.times do
+      result = statement.execute
+      expect(result.to_a.length).to eq(1)
+    end
+  end
+
+  it "should be reusable 10000 times" do
+    statement = @client.prepare 'SELECT 1'
+    10000.times do
+      result = statement.execute
+      expect(result.to_a.length).to eq(1)
+    end
+  end
+
+  it "should handle comparisons and likes" do
+    @client.query 'USE test'
+    @client.query 'CREATE TABLE IF NOT EXISTS mysql2_stmt_q(a int, b varchar(10))'
+    @client.query 'INSERT INTO mysql2_stmt_q (a, b) VALUES (1, "Hello"), (2, "World")'
+    statement = @client.prepare 'SELECT * FROM mysql2_stmt_q WHERE a < ?'
+    results = statement.execute(2)
+    expect(results.first).to eq("a" => 1, "b" => "Hello")
+
+    statement = @client.prepare 'SELECT * FROM mysql2_stmt_q WHERE b LIKE ?'
+    results = statement.execute('%orld')
+    expect(results.first).to eq("a" => 2, "b" => "World")
+
+    @client.query 'DROP TABLE IF EXISTS mysql2_stmt_q'
+  end
+
+  it "should select dates" do
+    statement = @client.prepare 'SELECT NOW()'
+    result = statement.execute
+    expect(result.first.first[1]).to be_an_instance_of(Time)
+  end
+
+  it "should tell us about the fields" do
+    statement = @client.prepare 'SELECT 1 as foo, 2'
+    statement.execute
+    list = statement.fields
+    expect(list.length).to eq(2)
+    expect(list.first).to eq('foo')
+    expect(list[1]).to eq('2')
+  end
+
+  context "utf8_db" do
+    before(:each) do
+      @client.query("DROP DATABASE IF EXISTS test_mysql2_stmt_utf8")
+      @client.query("CREATE DATABASE test_mysql2_stmt_utf8")
+      @client.query("USE test_mysql2_stmt_utf8")
+      @client.query("CREATE TABLE テーブル (整数 int, 文字列 varchar(32)) charset=utf8")
+      @client.query("INSERT INTO テーブル (整数, 文字列) VALUES (1, 'イチ'), (2, '弐'), (3, 'さん')")
+    end
+
+    after(:each) do
+      @client.query("DROP DATABASE test_mysql2_stmt_utf8")
+    end
+
+    it "should be able to retrieve utf8 field names correctly" do
+      stmt = @client.prepare 'SELECT * FROM `テーブル`'
+      expect(stmt.fields).to eq(%w(整数 文字列))
+      result = stmt.execute
+
+      expect(result.to_a).to eq([{ "整数" => 1, "文字列" => "イチ" }, { "整数" => 2, "文字列" => "弐" }, { "整数" => 3, "文字列" => "さん" }])
+    end
+
+    it "should be able to retrieve utf8 param query correctly" do
+      stmt = @client.prepare 'SELECT 整数 FROM テーブル WHERE 文字列 = ?'
+      expect(stmt.param_count).to eq(1)
+
+      result = stmt.execute 'イチ'
+
+      expect(result.to_a).to eq([{ "整数" => 1 }])
+    end
+
+    it "should be able to retrieve query with param in different encoding correctly" do
+      stmt = @client.prepare 'SELECT 整数 FROM テーブル WHERE 文字列 = ?'
+      expect(stmt.param_count).to eq(1)
+
+      param = 'イチ'.encode("EUC-JP")
+      result = stmt.execute param
+
+      expect(result.to_a).to eq([{ "整数" => 1 }])
+    end
+  end if defined? Encoding
+
+  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.execute.each do |r|
+        case n
+        when 1
+          expect(r).to eq([1])
+        when 2
+          expect(r).to eq([2])
+        else
+          violated "returned more than two rows"
+        end
+        n += 1
+      end
+    end
+  end
+
+  context "#each" do
+    # 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|
+        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|
+        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
+
+      @result = @client.prepare("SELECT 1").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)
+    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
+      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
+      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
+
+      expect {
+        result.each {}
+        result.each {}
+      }.to raise_exception(Mysql2::Error)
+
+      @client.query_options[:stream] = false
+      @client.query_options[:cache_rows] = true
+    end
+  end
+
+  context "#fields" do
+    before(:each) do
+      @client.query "USE test"
+      @test_result = @client.prepare("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").execute
+    end
+
+    it "method should exist" do
+      expect(@test_result).to respond_to(:fields)
+    end
+
+    it "should return an array of field names in proper order" do
+      result = @client.prepare("SELECT 'a', 'b', 'c'").execute
+      expect(result.fields).to eql(%w(a b c))
+    end
+  end
+
+  context "row data type mapping" do
+    before(:each) do
+      @client.query "USE test"
+      @test_result = @client.prepare("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").execute.first
+    end
+
+    it "should return nil for a NULL value" do
+      expect(@test_result['null_test']).to be_an_instance_of(NilClass)
+      expect(@test_result['null_test']).to eql(nil)
+    end
+
+    it "should return String for a BIT(64) value" do
+      expect(@test_result['bit_test']).to be_an_instance_of(String)
+      expect(@test_result['bit_test']).to eql("\000\000\000\000\000\000\000\005")
+    end
+
+    it "should return String for a BIT(1) value" do
+      expect(@test_result['single_bit_test']).to be_an_instance_of(String)
+      expect(@test_result['single_bit_test']).to eql("\001")
+    end
+
+    it "should return Fixnum for a TINYINT value" do
+      expect([Fixnum, Bignum]).to include(@test_result['tiny_int_test'].class)
+      expect(@test_result['tiny_int_test']).to eql(1)
+    end
+
+    it "should return TrueClass or FalseClass for a TINYINT value if :cast_booleans is enabled" do
+      @client.query 'INSERT INTO mysql2_test (bool_cast_test) VALUES (1)'
+      id1 = @client.last_id
+      @client.query 'INSERT INTO mysql2_test (bool_cast_test) VALUES (0)'
+      id2 = @client.last_id
+      @client.query 'INSERT INTO mysql2_test (bool_cast_test) VALUES (-1)'
+      id3 = @client.last_id
+
+      result1 = @client.query 'SELECT bool_cast_test FROM mysql2_test WHERE bool_cast_test = 1 LIMIT 1', :cast_booleans => true
+      result2 = @client.query 'SELECT bool_cast_test FROM mysql2_test WHERE bool_cast_test = 0 LIMIT 1', :cast_booleans => true
+      result3 = @client.query 'SELECT bool_cast_test FROM mysql2_test WHERE bool_cast_test = -1 LIMIT 1', :cast_booleans => true
+      expect(result1.first['bool_cast_test']).to be true
+      expect(result2.first['bool_cast_test']).to be false
+      expect(result3.first['bool_cast_test']).to be true
+
+      @client.query "DELETE from mysql2_test WHERE id IN(#{id1},#{id2},#{id3})"
+    end
+
+    it "should return TrueClass or FalseClass for a BIT(1) value if :cast_booleans is enabled" do
+      @client.query 'INSERT INTO mysql2_test (single_bit_test) VALUES (1)'
+      id1 = @client.last_id
+      @client.query 'INSERT INTO mysql2_test (single_bit_test) VALUES (0)'
+      id2 = @client.last_id
+
+      result1 = @client.query "SELECT single_bit_test FROM mysql2_test WHERE id = #{id1}", :cast_booleans => true
+      result2 = @client.query "SELECT single_bit_test FROM mysql2_test WHERE id = #{id2}", :cast_booleans => true
+      expect(result1.first['single_bit_test']).to be true
+      expect(result2.first['single_bit_test']).to be false
+
+      @client.query "DELETE from mysql2_test WHERE id IN(#{id1},#{id2})"
+    end
+
+    it "should return Fixnum for a SMALLINT value" do
+      expect([Fixnum, Bignum]).to include(@test_result['small_int_test'].class)
+      expect(@test_result['small_int_test']).to eql(10)
+    end
+
+    it "should return Fixnum for a MEDIUMINT value" do
+      expect([Fixnum, Bignum]).to include(@test_result['medium_int_test'].class)
+      expect(@test_result['medium_int_test']).to eql(10)
+    end
+
+    it "should return Fixnum for an INT value" do
+      expect([Fixnum, Bignum]).to include(@test_result['int_test'].class)
+      expect(@test_result['int_test']).to eql(10)
+    end
+
+    it "should return Fixnum for a BIGINT value" do
+      expect([Fixnum, Bignum]).to include(@test_result['big_int_test'].class)
+      expect(@test_result['big_int_test']).to eql(10)
+    end
+
+    it "should return Fixnum for a YEAR value" do
+      expect([Fixnum, Bignum]).to include(@test_result['year_test'].class)
+      expect(@test_result['year_test']).to eql(2009)
+    end
+
+    it "should return BigDecimal for a DECIMAL value" do
+      expect(@test_result['decimal_test']).to be_an_instance_of(BigDecimal)
+      expect(@test_result['decimal_test']).to eql(10.3)
+    end
+
+    it "should return Float for a FLOAT value" do
+      expect(@test_result['float_test']).to be_an_instance_of(Float)
+      expect(@test_result['float_test']).to be_within(1e-5).of(10.3)
+    end
+
+    it "should return Float for a DOUBLE value" do
+      expect(@test_result['double_test']).to be_an_instance_of(Float)
+      expect(@test_result['double_test']).to eql(10.3)
+    end
+
+    it "should return Time for a DATETIME value when within the supported range" do
+      expect(@test_result['date_time_test']).to be_an_instance_of(Time)
+      expect(@test_result['date_time_test'].strftime("%Y-%m-%d %H:%M:%S")).to eql('2010-04-04 11:44:00')
+    end
+
+    if 1.size == 4 # 32bit
+      klass = if RUBY_VERSION =~ /1.8/
+        DateTime
+      else
+        Time
+      end
+
+      it "should return DateTime when timestamp is < 1901-12-13 20:45:52" do
+        # 1901-12-13T20:45:52 is the min for 32bit Ruby 1.8
+        r = @client.query("SELECT CAST('1901-12-13 20:45:51' AS DATETIME) as test")
+        expect(r.first['test']).to be_an_instance_of(klass)
+      end
+
+      it "should return DateTime when timestamp is > 2038-01-19T03:14:07" do
+        # 2038-01-19T03:14:07 is the max for 32bit Ruby 1.8
+        r = @client.query("SELECT CAST('2038-01-19 03:14:08' AS DATETIME) as test")
+        expect(r.first['test']).to be_an_instance_of(klass)
+      end
+    elsif 1.size == 8 # 64bit
+      if RUBY_VERSION =~ /1.8/
+        it "should return Time when timestamp is > 0138-12-31 11:59:59" do
+          r = @client.query("SELECT CAST('0139-1-1 00:00:00' AS DATETIME) as test")
+          expect(r.first['test']).to be_an_instance_of(Time)
+        end
+
+        it "should return DateTime when timestamp is < 0139-1-1T00:00:00" do
+          r = @client.query("SELECT CAST('0138-12-31 11:59:59' AS DATETIME) as test")
+          expect(r.first['test']).to be_an_instance_of(DateTime)
+        end
+
+        it "should return Time when timestamp is > 2038-01-19T03:14:07" do
+          r = @client.query("SELECT CAST('2038-01-19 03:14:08' AS DATETIME) as test")
+          expect(r.first['test']).to be_an_instance_of(Time)
+        end
+      else
+        it "should return Time when timestamp is < 1901-12-13 20:45:52" do
+          r = @client.query("SELECT CAST('1901-12-13 20:45:51' AS DATETIME) as test")
+          expect(r.first['test']).to be_an_instance_of(Time)
+        end
+
+        it "should return Time when timestamp is > 2038-01-19T03:14:07" do
+          r = @client.query("SELECT CAST('2038-01-19 03:14:08' AS DATETIME) as test")
+          expect(r.first['test']).to be_an_instance_of(Time)
+        end
+      end
+    end
+
+    it "should return Time for a TIMESTAMP value when within the supported range" do
+      expect(@test_result['timestamp_test']).to be_an_instance_of(Time)
+      expect(@test_result['timestamp_test'].strftime("%Y-%m-%d %H:%M:%S")).to eql('2010-04-04 11:44:00')
+    end
+
+    it "should return Time for a TIME value" do
+      expect(@test_result['time_test']).to be_an_instance_of(Time)
+      expect(@test_result['time_test'].strftime("%Y-%m-%d %H:%M:%S")).to eql('2000-01-01 11:44:00')
+    end
+
+    it "should return Date for a DATE value" do
+      expect(@test_result['date_test']).to be_an_instance_of(Date)
+      expect(@test_result['date_test'].strftime("%Y-%m-%d")).to eql('2010-04-04')
+    end
+
+    it "should return String for an ENUM value" do
+      expect(@test_result['enum_test']).to be_an_instance_of(String)
+      expect(@test_result['enum_test']).to eql('val1')
+    end
+
+    it "should raise an error given an invalid DATETIME" do
+      expect { @client.query("SELECT CAST('1972-00-27 00:00:00' AS DATETIME) as bad_datetime").each }.to \
+        raise_error(Mysql2::Error, "Invalid date in field 'bad_datetime': 1972-00-27 00:00:00")
+    end
+
+    context "string encoding for ENUM values" do
+      before { pending('Encoding is undefined') unless defined?(Encoding) }
+
+      it "should default to the connection's encoding if Encoding.default_internal is nil" do
+        with_internal_encoding nil do
+          result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['enum_test'].encoding).to eql(Encoding::UTF_8)
+
+          client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii'))
+          result = client2.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['enum_test'].encoding).to eql(Encoding::US_ASCII)
+          client2.close
+        end
+      end
+
+      it "should use Encoding.default_internal" do
+        with_internal_encoding Encoding::UTF_8 do
+          result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['enum_test'].encoding).to eql(Encoding.default_internal)
+        end
+
+        with_internal_encoding Encoding::ASCII do
+          result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['enum_test'].encoding).to eql(Encoding.default_internal)
+        end
+      end
+    end
+
+    it "should return String for a SET value" do
+      expect(@test_result['set_test']).to be_an_instance_of(String)
+      expect(@test_result['set_test']).to eql('val1,val2')
+    end
+
+    context "string encoding for SET values" do
+      before { pending('Encoding is undefined') unless defined?(Encoding) }
+
+      it "should default to the connection's encoding if Encoding.default_internal is nil" do
+        with_internal_encoding nil do
+          result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['set_test'].encoding).to eql(Encoding::UTF_8)
+
+          client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii'))
+          result = client2.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['set_test'].encoding).to eql(Encoding::US_ASCII)
+          client2.close
+        end
+      end
+
+      it "should use Encoding.default_internal" do
+        with_internal_encoding Encoding::UTF_8 do
+          result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['set_test'].encoding).to eql(Encoding.default_internal)
+        end
+
+        with_internal_encoding Encoding::ASCII do
+          result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['set_test'].encoding).to eql(Encoding.default_internal)
+        end
+      end
+    end
+
+    it "should return String for a BINARY value" do
+      expect(@test_result['binary_test']).to be_an_instance_of(String)
+      expect(@test_result['binary_test']).to eql("test#{"\000" * 6}")
+    end
+
+    context "string encoding for BINARY values" do
+      before { pending('Encoding is undefined') unless defined?(Encoding) }
+
+      it "should default to binary if Encoding.default_internal is nil" do
+        with_internal_encoding nil do
+          result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['binary_test'].encoding).to eql(Encoding::BINARY)
+        end
+      end
+
+      it "should not use Encoding.default_internal" do
+        with_internal_encoding Encoding::UTF_8 do
+          result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['binary_test'].encoding).to eql(Encoding::BINARY)
+        end
+
+        with_internal_encoding Encoding::ASCII do
+          result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+          expect(result['binary_test'].encoding).to eql(Encoding::BINARY)
+        end
+      end
+    end
+
+    {
+      'char_test' => 'CHAR',
+      'varchar_test' => 'VARCHAR',
+      'varbinary_test' => 'VARBINARY',
+      'tiny_blob_test' => 'TINYBLOB',
+      'tiny_text_test' => 'TINYTEXT',
+      'blob_test' => 'BLOB',
+      'text_test' => 'TEXT',
+      'medium_blob_test' => 'MEDIUMBLOB',
+      'medium_text_test' => 'MEDIUMTEXT',
+      'long_blob_test' => 'LONGBLOB',
+      'long_text_test' => 'LONGTEXT',
+    }.each do |field, type|
+      it "should return a String for #{type}" do
+        expect(@test_result[field]).to be_an_instance_of(String)
+        expect(@test_result[field]).to eql("test")
+      end
+
+      context "string encoding for #{type} values" do
+        before { pending('Encoding is undefined') unless defined?(Encoding) }
+
+        if %w(VARBINARY TINYBLOB BLOB MEDIUMBLOB LONGBLOB).include?(type)
+          it "should default to binary if Encoding.default_internal is nil" do
+            with_internal_encoding nil do
+              result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+              expect(result['binary_test'].encoding).to eql(Encoding::BINARY)
+            end
+          end
+
+          it "should not use Encoding.default_internal" do
+            with_internal_encoding Encoding::UTF_8 do
+              result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+              expect(result['binary_test'].encoding).to eql(Encoding::BINARY)
+            end
+
+            with_internal_encoding Encoding::ASCII do
+              result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+              expect(result['binary_test'].encoding).to eql(Encoding::BINARY)
+            end
+          end
+        else
+          it "should default to utf-8 if Encoding.default_internal is nil" do
+            with_internal_encoding nil do
+              result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+              expect(result[field].encoding).to eql(Encoding::UTF_8)
+
+              client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii'))
+              result = client2.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+              expect(result[field].encoding).to eql(Encoding::US_ASCII)
+              client2.close
+            end
+          end
+
+          it "should use Encoding.default_internal" do
+            with_internal_encoding Encoding::UTF_8 do
+              result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+              expect(result[field].encoding).to eql(Encoding.default_internal)
+            end
+
+            with_internal_encoding Encoding::ASCII do
+              result = @client.query("SELECT * FROM mysql2_test ORDER BY id DESC LIMIT 1").first
+              expect(result[field].encoding).to eql(Encoding.default_internal)
+            end
+          end
+        end
+      end
+    end
+  end
+
+  context 'last_id' do
+    before(:each) do
+      @client.query 'USE test'
+      @client.query 'CREATE TABLE IF NOT EXISTS lastIdTest (`id` BIGINT NOT NULL AUTO_INCREMENT, blah INT(11), PRIMARY KEY (`id`))'
+    end
+
+    after(:each) do
+      @client.query 'DROP TABLE lastIdTest'
+    end
+
+    it 'should return last insert id' do
+      stmt = @client.prepare 'INSERT INTO lastIdTest (blah) VALUES (?)'
+      expect(stmt.last_id).to eq 0
+      stmt.execute 1
+      expect(stmt.last_id).to eq 1
+    end
+
+    it 'should handle bigint ids' do
+      stmt = @client.prepare 'INSERT INTO lastIdTest (id, blah) VALUES (?, ?)'
+      stmt.execute 5000000000, 5000
+      expect(stmt.last_id).to eql(5000000000)
+
+      stmt = @client.prepare 'INSERT INTO lastIdTest (blah) VALUES (?)'
+      stmt.execute 5001
+      expect(stmt.last_id).to eql(5000000001)
+    end
+  end
+
+  context 'affected_rows' do
+    before :each do
+      @client.query 'USE test'
+      @client.query 'CREATE TABLE IF NOT EXISTS lastIdTest (`id` BIGINT NOT NULL AUTO_INCREMENT, blah INT(11), PRIMARY KEY (`id`))'
+    end
+
+    after :each do
+      @client.query 'DROP TABLE lastIdTest'
+    end
+
+    it 'should return number of rows affected by an insert' do
+      stmt = @client.prepare 'INSERT INTO lastIdTest (blah) VALUES (?)'
+      expect(stmt.affected_rows).to eq 0
+      stmt.execute 1
+      expect(stmt.affected_rows).to eq 1
+    end
+
+    it 'should return number of rows affected by an update' do
+      stmt = @client.prepare 'INSERT INTO lastIdTest (blah) VALUES (?)'
+      stmt.execute 1
+      expect(stmt.affected_rows).to eq 1
+      stmt.execute 2
+      expect(stmt.affected_rows).to eq 1
+
+      stmt = @client.prepare 'UPDATE lastIdTest SET blah=? WHERE blah=?'
+      stmt.execute 0, 1
+      expect(stmt.affected_rows).to eq 1
+    end
+
+    it 'should return number of rows affected by a delete' do
+      stmt = @client.prepare 'INSERT INTO lastIdTest (blah) VALUES (?)'
+      stmt.execute 1
+      expect(stmt.affected_rows).to eq 1
+      stmt.execute 2
+      expect(stmt.affected_rows).to eq 1
+
+      stmt = @client.prepare 'DELETE FROM lastIdTest WHERE blah=?'
+      stmt.execute 1
+      expect(stmt.affected_rows).to eq 1
+    end
+  end
+
+  context 'close' do
+    it 'should free server resources' do
+      stmt = @client.prepare 'SELECT 1'
+      expect { stmt.close }.to change {
+        @client.query("SHOW STATUS LIKE 'Prepared_stmt_count'").first['Value'].to_i
+      }.by(-1)
+    end
+
+    it 'should raise an error on subsequent execution' do
+      stmt = @client.prepare 'SELECT 1'
+      stmt.close
+      expect { stmt.execute }.to raise_error(Mysql2::Error, /Invalid statement handle/)
+    end
+  end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 4bcb702..73c4581 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -7,13 +7,20 @@ require 'yaml'
 DatabaseCredentials = YAML.load_file('spec/configuration.yml')
 
 RSpec.configure do |config|
+  config.disable_monkey_patching!
+
   def with_internal_encoding(encoding)
     old_enc = Encoding.default_internal
+    old_verbose = $VERBOSE
+    $VERBOSE = nil
     Encoding.default_internal = encoding
+    $VERBOSE = old_verbose
 
     yield
   ensure
+    $VERBOSE = nil
     Encoding.default_internal = old_enc
+    $VERBOSE = old_verbose
   end
 
   config.before :each do
diff --git a/spec/ssl/ca-cert.pem b/spec/ssl/ca-cert.pem
new file mode 100644
index 0000000..cf9b8d5
--- /dev/null
+++ b/spec/ssl/ca-cert.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE-----
+MIICqjCCAZICCQDbDS+Z2mpWkDANBgkqhkiG9w0BAQsFADAXMRUwEwYDVQQDDAxj
+YV9teXNxbDJnZW0wHhcNMTUwOTA5MDQ1NzIxWhcNMjUwNzE4MDQ1NzIxWjAXMRUw
+EwYDVQQDDAxjYV9teXNxbDJnZW0wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
+AoIBAQDFnpc22lPFtdPELsIffsDt8cD2Hkt47nGMcKQ9n4U98yAg+fodipyP1Bn0
+2OeaONqpttJIET7HxlGrtugPtV/O8XZHlhfAHrRUDMFZJhgnnqK+c/7fRGeB0Eqw
+ljBlRD/dDL3bFq5hVBC9QsGi5k03r+xLPKm5ccAr4WtofcoKXqEbSO6koTSrsGG5
+7inlldM2AVzrY2kXbe0jAyNvYmDL2ycN8G2wObogPWDfITQRhOxfkzKIQiEhQF2Y
+/DlhT7IbIarBIm6abf6JxZ6/Sm5XyVNEWOnryXM6rKyVeGktCxLHNmxx5eKYs440
+8hNgURa8pB+aZaiokkwhM1+jmE83AgMBAAEwDQYJKoZIhvcNAQELBQADggEBACrQ
+umqygXkkbff5Jqf6AYi30U3c+byX+IButRKXN9Ete2LPcT76o/snS9Lexf3KQsIy
+a2Tcc9adak7pBf7FgHdiZkWiQp3MDgx2gJu6Uu6TNzfT8jy2JrHyBWw4ydEvhyA8
+cgelTHSaudafKeQgU4KYc8bqafYFILkWxPzgtwitENIDfx/SHt65BWaQZjYJlFou
+zPZXeoT3lAwKGYqIvwPvBTC23cXg/Swt/mcKe3/Xxjx85Dw/9vi6a9+VQwlOojgd
+w2o07xkIcJcI0Oxyp3mD0U5wAmBQGI76Yi9ZDROHF65KEXfQ3tYKl2vR7CXpcJ4+
+7+fVsE8+dADJdZIiuaA=
+-----END CERTIFICATE-----
diff --git a/spec/ssl/ca-key.pem b/spec/ssl/ca-key.pem
new file mode 100644
index 0000000..bbdadb7
--- /dev/null
+++ b/spec/ssl/ca-key.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEAxZ6XNtpTxbXTxC7CH37A7fHA9h5LeO5xjHCkPZ+FPfMgIPn6
+HYqcj9QZ9NjnmjjaqbbSSBE+x8ZRq7boD7VfzvF2R5YXwB60VAzBWSYYJ56ivnP+
+30RngdBKsJYwZUQ/3Qy92xauYVQQvULBouZNN6/sSzypuXHAK+FraH3KCl6hG0ju
+pKE0q7Bhue4p5ZXTNgFc62NpF23tIwMjb2Jgy9snDfBtsDm6ID1g3yE0EYTsX5My
+iEIhIUBdmPw5YU+yGyGqwSJumm3+icWev0puV8lTRFjp68lzOqyslXhpLQsSxzZs
+ceXimLOONPITYFEWvKQfmmWoqJJMITNfo5hPNwIDAQABAoIBAQClkmtFRQVdKCum
+Ojrg4nVIpv2x983qI3U1YobpLocXUWVA29BIAgOMqfuZXkYlu67Q9OEYCoLcJHf2
+88dYqfD81OfxsHpzuAYESa+RPs6MG2hlQ5BuhcRnShnZ++vOXLFZRjynnEg8OY/Q
+0makUmqt1pKWstvNCNYmrbYtFP87UXQCF06zkhM0cZJvVt0gPZGUituWI0uAoE51
+U8+WSwD/T761BHM6BuMn56mfZkP5jeVIFl0iFha9rGR0Z6K8mVQAYQAUtUx9tN/3
+a8fEOcYulq/9R5tMRWtsF8LD8DGQBNkY3e/WKDuZtLw2Dl3L09gxVH9DXCLiYU5d
+OG3JmqDpAoGBAP08yq143H4n6yGT9DC8YjaLgN0VoenK21CEqhwtGWipc/kbGooe
+/jaHl6bo9v1GOGlJieqSUqsXNltS7FOLhGFAQFwMYZ3V/h15Vx23Z+xkCCHIB6HH
+YJZqkQY7Jt86wXcaLU5j9fxM+BY+8Ets4bVhZN9Ai6AnlTz0+d8UJG+bAoGBAMfG
+efYrdjTKI5eK9aiVJyoh57BEPOsTsave2U+R8Q+fErQ0QD0UmbWgwYGgkPuDrFYT
+owg09EEz88KONv18VZ+mB1qfyQUoOL6rWIGxXC08upy2i9100PaBFiYlkLNoK7yJ
+bze0rFSiFclJJXZGzEaVvcEdKnXxfhttaJwQGK6VAoGBAOQEUvJzuwWU5/CqCdvA
+JCa84eEv00RxtZwAeDM6oIBO4+/O6cyoL3nmCTTu20YebjjPUHF4IxuOoREFz2lC
+XIY8ljbLpzG5N0BOu5Q0SkzdnTzdoZGXtm55se+MX2nsu7qERXsqIpl0rIVLUo53
+kZwCABPNSGuCeKwUYNDukAg1AoGBALiHHSqEVKhIOn4FDgqM0uM49CA9t6NPyqI9
+sq6r2GWcgpNPXDLPL3e0KGlK3gBkTLApbULsXt1HVpZT9HlJ+nD/0/UieHS6BUgh
+Txxkrgbe/GQ6vZBuEYJQFBxiQHlm9Fcu/zsOOMvn94W4edD5bkCYmfChtxHAYcKF
+2cWlnJbNAoGAWMV4GIY2DYlztXdyMVuPwsjPcSPMmL8Nc2ATWYRfcoG0Zl0yvwPh
+2VOu7Q/7bNF2LOe6lPe1hoeB6rT44IYZaWMo3ikY8xW9RztOLSv8E9uE1K9yq8OA
+P8QzXmr1Lga+hoEmMHc2biEJNeF6iAcAFfrHj9Sr7w5PC8g4A3PlCvU=
+-----END RSA PRIVATE KEY-----
diff --git a/spec/ssl/ca.cnf b/spec/ssl/ca.cnf
new file mode 100644
index 0000000..07374ad
--- /dev/null
+++ b/spec/ssl/ca.cnf
@@ -0,0 +1,22 @@
+
+[ ca ]
+# January 1, 2015
+default_startdate = 2015010360000Z
+
+[ req ]
+distinguished_name = req_distinguished_name
+
+[ req_distinguished_name ]
+# If this isn't set, the error is error, no objects specified in config file
+commonName = Common Name (hostname, IP, or your name)
+
+countryName_default            = US
+stateOrProvinceName_default    = CA
+localityName_default           = San Francisco
+0.organizationName_default     = mysql2_gem
+organizationalUnitName_default = Mysql2Gem
+emailAddress_default           = mysql2gem at example.com
+
+
+commonName_default             = ca_mysql2gem
+
diff --git a/spec/ssl/cert.cnf b/spec/ssl/cert.cnf
new file mode 100644
index 0000000..ca1dce7
--- /dev/null
+++ b/spec/ssl/cert.cnf
@@ -0,0 +1,22 @@
+
+[ ca ]
+# January 1, 2015
+default_startdate = 2015010360000Z
+
+[ req ]
+distinguished_name = req_distinguished_name
+
+[ req_distinguished_name ]
+# If this isn't set, the error is error, no objects specified in config file
+commonName = Common Name (hostname, IP, or your name)
+
+countryName_default            = US
+stateOrProvinceName_default    = CA
+localityName_default           = San Francisco
+0.organizationName_default     = mysql2_gem
+organizationalUnitName_default = Mysql2Gem
+emailAddress_default           = mysql2gem at example.com
+
+
+commonName_default             = mysql2gem.example.com
+
diff --git a/spec/ssl/client-cert.pem b/spec/ssl/client-cert.pem
new file mode 100644
index 0000000..3887246
--- /dev/null
+++ b/spec/ssl/client-cert.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE-----
+MIICqzCCAZMCAQEwDQYJKoZIhvcNAQELBQAwFzEVMBMGA1UEAwwMY2FfbXlzcWwy
+Z2VtMB4XDTE1MDkwOTA0NTcyMVoXDTI1MDcxODA0NTcyMVowIDEeMBwGA1UEAwwV
+bXlzcWwyZ2VtLmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEA1ZuBf1FVJqil7/LvnXqPd43ujo0xqbFy7QrqmM5U/UM3ggMCf2Gr2/Wo
+ZJGPTk1NFaiUyM5mhSlgi0/SGPEp9JMUuH+Uiv9UwmOFl9Em3FXQQ8SG7fV7651u
+AUskNgfEqoy+f+uvi1P155rHNDx7Yw6i+wwfpLGTU0boMnLL6cO/KcIbZlx4/2Lq
+r5sYbpIqhz46bbG+fIhvepruH9h7WVWqAibTqymYrA3T03O/HWTOqfq03gM7Oe3t
+JvJbqX2LecQvi2SbQoX8c2MrQ6X7xDe2Ajh7Yx9DQ1gqClTglbPFHNiWPcGACg+W
+2ptCY/Q2SdP5h1dtj5Sw5VwL3dvCjQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQA2
+qTbfgDm0IG8x1qP61ztT9F2WRwG7cp6qHT5oB5wDcOUFes9QJjeB8RoIkB+hRlqG
+J6/Tbxs49d7oKhOQ0UaTnfIKC5m0UFYFGc3lUcwxQyggOWx9XV5ZmGb48+RLFnDV
+Gfcs/hvfem6Xfpgzr8bGs2ZM9x1j9YnXNJVePmKwktjCPnXPOeHyxNZPA+CWHed/
+dNg1IWuQnnp20LgNRARCTgR/ONAJNUfh2GqRLq2JOf0cyhNlsKQ3epkeUyc72knI
+oWVxPluQYvFHN+xif0FMGVLM8lz0b+6uPJDA2Km70B/iorMRVb0vbMeFrMmQ5UgM
+4tplX52P2vb6JNnektfR
+-----END CERTIFICATE-----
diff --git a/spec/ssl/client-key.pem b/spec/ssl/client-key.pem
new file mode 100644
index 0000000..d752942
--- /dev/null
+++ b/spec/ssl/client-key.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEowIBAAKCAQEA1ZuBf1FVJqil7/LvnXqPd43ujo0xqbFy7QrqmM5U/UM3ggMC
+f2Gr2/WoZJGPTk1NFaiUyM5mhSlgi0/SGPEp9JMUuH+Uiv9UwmOFl9Em3FXQQ8SG
+7fV7651uAUskNgfEqoy+f+uvi1P155rHNDx7Yw6i+wwfpLGTU0boMnLL6cO/KcIb
+Zlx4/2Lqr5sYbpIqhz46bbG+fIhvepruH9h7WVWqAibTqymYrA3T03O/HWTOqfq0
+3gM7Oe3tJvJbqX2LecQvi2SbQoX8c2MrQ6X7xDe2Ajh7Yx9DQ1gqClTglbPFHNiW
+PcGACg+W2ptCY/Q2SdP5h1dtj5Sw5VwL3dvCjQIDAQABAoIBAEWhGjZZWctvQCAW
+bbtEv012a6P2LJEnMdJJM6253IRuC8MKnh7NxMq/qjOWK0OX+R+tQ0qt1Udk9H6U
+92SAAHAkHaYCmHYywvtWm66gU+2Q34Gnp2AcHFfyinBLgTNHlvkNRe/G8QMWzFrB
+3luNt57Tn5b8Hbh+1gpYW8pOF2BMgIsLRK+8b26TKUWrSCc/ZxOSY4wmrNybxkgr
+HGt27lwIN0cvJZbmQvHevNzzCn+bYoo2K1MQj34xHbZ2NLqKqFVlSJtr9+BHffAc
+fkcf+V+D+FkitUVkha9qXa02wtLzYSF+Q5Ef3kQQs6hs/HOdN16g17l9QC6Mk1vm
+a9yV5CECgYEA/9FglQmFimwBCOWEvjkZzoXFusuvRWRgAPU/1c9DAYRS2GfOkjlH
+RPAltczdXh4EQ0NkCqHH7JWgrdXGonKg4fcITumdwcYKV5QfmKBO4onAboEM0Wq7
+wjifuga7npQhPnGvkXFDamVz5McQPObvV42VAUwk1N00gOYw/46ryLkCgYEA1cJv
+jHAq0DKlUGXKyZ+ixsogRpwTQvND/qUquSLgD/KgfeT+70AnsEF6DbVLKoaJ35CF
+ju83VYLfeBljq+E/lgmAyaChplORRXcu+xPQE4rbp0MbsoBOYGNWLFAw3twGsQf9
+iuAtCVxij/hhj4FWRebYHMnV6Min2VPbZdASNnUCgYBIiX8gY3XJPTzB4ArWwWwu
+4kGh6NWHEKIkQ2ZZYw615GZ1VGH/llw+EPYwaamvYUWGKRq55QvCat8Hy6EqOOSj
+jh99+MIxyszt7mNTLMmRdMvqyY7v5prcxJ+N6RDUM16FzUiiLgKWrbPCACv7iOP+
+6HeCyat77ElR73OfUz4kiQKBgH+2r9cEnU/PMp4ac1KLokGLOkV1srxpg9J89E2w
+3JYqrGELlJV1i0DvnfDaxJIf1/hO7L09h537l3C2Gqry5X7LJrtQ0cQCYeVTFCrG
+56cFa78/hSjdJ/bG4xGOx+QfKZBT6dQzpDTXkbva9s86w0T4a16n6LowSLi8NXVb
+H8aRAoGBAKzlt6deB+ASIrGH6mM0eLxF1OcNTB+rE4AJxoUyO1oAmCv9UeK3IzwP
+ohhmo/kEOSCVG6WE+6+r9mojcoHu3ZrobVKl59R7KMdzunMXqxZcXeTqjvqdTtV7
+rWuEz/TKIe7o0Tx19XVGuNftyx2pLuspSAAbZ+YAQJtzmLzsGkss
+-----END RSA PRIVATE KEY-----
diff --git a/spec/ssl/client-req.pem b/spec/ssl/client-req.pem
new file mode 100644
index 0000000..fc07748
--- /dev/null
+++ b/spec/ssl/client-req.pem
@@ -0,0 +1,15 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICZTCCAU0CAQAwIDEeMBwGA1UEAwwVbXlzcWwyZ2VtLmV4YW1wbGUuY29tMIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1ZuBf1FVJqil7/LvnXqPd43u
+jo0xqbFy7QrqmM5U/UM3ggMCf2Gr2/WoZJGPTk1NFaiUyM5mhSlgi0/SGPEp9JMU
+uH+Uiv9UwmOFl9Em3FXQQ8SG7fV7651uAUskNgfEqoy+f+uvi1P155rHNDx7Yw6i
++wwfpLGTU0boMnLL6cO/KcIbZlx4/2Lqr5sYbpIqhz46bbG+fIhvepruH9h7WVWq
+AibTqymYrA3T03O/HWTOqfq03gM7Oe3tJvJbqX2LecQvi2SbQoX8c2MrQ6X7xDe2
+Ajh7Yx9DQ1gqClTglbPFHNiWPcGACg+W2ptCY/Q2SdP5h1dtj5Sw5VwL3dvCjQID
+AQABoAAwDQYJKoZIhvcNAQELBQADggEBAB05YaBSyAKCgHfBWhpZ1+OOVp1anr2v
+TkStnqmNrNM2qBJXjfrythPTX4EJAt7+eNdH/6IVA93qKC/EUQVuMjgfMmMUaM+m
+5pqfAo95w7vUY147U9nbC+EIo2u1KOVTNTgl45H372/1vCwTHZYu2atCk4tN3ueO
+0O2XW89Kq94/7PDAExN2PhZdeATVX9dPNT+7ZUDNe8cuq9v0YCHy+2JN2WkplxcG
+kMyCE3YYLnd96YtWiS9DOUib3+b7FwyGe0dXeLVw1br3NZGCZrybyfmnAQfiouAF
+9nMxKIpWFSx00ubGrUefOQqp6nuk27n+scgr4+d6dBXz9efEEvTbLKA=
+-----END CERTIFICATE REQUEST-----
diff --git a/spec/ssl/gen_certs.sh b/spec/ssl/gen_certs.sh
new file mode 100644
index 0000000..d55872d
--- /dev/null
+++ b/spec/ssl/gen_certs.sh
@@ -0,0 +1,48 @@
+#!/usr/bin/env bash
+
+set -eu
+
+echo "
+[ ca ]
+# January 1, 2015
+default_startdate = 2015010360000Z
+
+[ req ]
+distinguished_name = req_distinguished_name
+
+[ req_distinguished_name ]
+# If this isn't set, the error is "error, no objects specified in config file"
+commonName = Common Name (hostname, IP, or your name)
+
+countryName_default            = US
+stateOrProvinceName_default    = CA
+localityName_default           = San Francisco
+0.organizationName_default     = mysql2_gem
+organizationalUnitName_default = Mysql2Gem
+emailAddress_default           = mysql2gem at example.com
+" | tee ca.cnf cert.cnf
+
+# The client and server certs must have a diferent common name than the CA
+# to avoid "SSL connection error: error:00000001:lib(0):func(0):reason(1)"
+
+echo "
+commonName_default             = ca_mysql2gem
+" >> ca.cnf
+
+echo "
+commonName_default             = mysql2gem.example.com
+" >> cert.cnf
+
+# Generate a set of certificates
+openssl genrsa -out ca-key.pem 2048
+openssl req -new -x509 -nodes -days 3600 -key ca-key.pem -out ca-cert.pem -batch -config ca.cnf
+openssl req -newkey rsa:2048 -days 3600 -nodes -keyout pkcs8-server-key.pem -out server-req.pem -batch -config cert.cnf
+openssl x509 -req -in server-req.pem -days 3600 -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 -out server-cert.pem
+openssl req -newkey rsa:2048 -days 3600 -nodes -keyout pkcs8-client-key.pem -out client-req.pem -batch -config cert.cnf
+openssl x509 -req -in client-req.pem -days 3600 -CA ca-cert.pem -CAkey ca-key.pem -set_serial 01 -out client-cert.pem
+
+# Convert format from PKCS#8 to PKCS#1
+openssl rsa -in pkcs8-server-key.pem -out server-key.pem
+openssl rsa -in pkcs8-client-key.pem -out client-key.pem
+
+echo "done"
diff --git a/spec/ssl/pkcs8-client-key.pem b/spec/ssl/pkcs8-client-key.pem
new file mode 100644
index 0000000..a4858b4
--- /dev/null
+++ b/spec/ssl/pkcs8-client-key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDVm4F/UVUmqKXv
+8u+deo93je6OjTGpsXLtCuqYzlT9QzeCAwJ/Yavb9ahkkY9OTU0VqJTIzmaFKWCL
+T9IY8Sn0kxS4f5SK/1TCY4WX0SbcVdBDxIbt9XvrnW4BSyQ2B8SqjL5/66+LU/Xn
+msc0PHtjDqL7DB+ksZNTRugycsvpw78pwhtmXHj/YuqvmxhukiqHPjptsb58iG96
+mu4f2HtZVaoCJtOrKZisDdPTc78dZM6p+rTeAzs57e0m8lupfYt5xC+LZJtChfxz
+YytDpfvEN7YCOHtjH0NDWCoKVOCVs8Uc2JY9wYAKD5bam0Jj9DZJ0/mHV22PlLDl
+XAvd28KNAgMBAAECggEARaEaNllZy29AIBZtu0S/TXZro/YskScx0kkzrbnchG4L
+wwqeHs3Eyr+qM5YrQ5f5H61DSq3VR2T0fpT3ZIAAcCQdpgKYdjLC+1abrqBT7ZDf
+gaenYBwcV/KKcEuBM0eW+Q1F78bxAxbMWsHeW423ntOflvwduH7WClhbyk4XYEyA
+iwtEr7xvbpMpRatIJz9nE5JjjCas3JvGSCsca3buXAg3Ry8lluZC8d683PMKf5ti
+ijYrUxCPfjEdtnY0uoqoVWVIm2v34Ed98Bx+Rx/5X4P4WSK1RWSFr2pdrTbC0vNh
+IX5DkR/eRBCzqGz8c503XqDXuX1ALoyTW+Zr3JXkIQKBgQD/0WCVCYWKbAEI5YS+
+ORnOhcW6y69FZGAA9T/Vz0MBhFLYZ86SOUdE8CW1zN1eHgRDQ2QKocfslaCt1cai
+cqDh9whO6Z3BxgpXlB+YoE7iicBugQzRarvCOJ+6BruelCE+ca+RcUNqZXPkxxA8
+5u9XjZUBTCTU3TSA5jD/jqvIuQKBgQDVwm+McCrQMqVQZcrJn6LGyiBGnBNC80P+
+pSq5IuAP8qB95P7vQCewQXoNtUsqhonfkIWO7zdVgt94GWOr4T+WCYDJoKGmU5FF
+dy77E9ATitunQxuygE5gY1YsUDDe3AaxB/2K4C0JXGKP+GGPgVZF5tgcydXoyKfZ
+U9tl0BI2dQKBgEiJfyBjdck9PMHgCtbBbC7iQaHo1YcQoiRDZlljDrXkZnVUYf+W
+XD4Q9jBpqa9hRYYpGrnlC8Jq3wfLoSo45KOOH334wjHKzO3uY1MsyZF0y+rJju/m
+mtzEn43pENQzXoXNSKIuApats8IAK/uI4/7od4LJq3vsSVHvc59TPiSJAoGAf7av
+1wSdT88ynhpzUouiQYs6RXWyvGmD0nz0TbDcliqsYQuUlXWLQO+d8NrEkh/X+E7s
+vT2HnfuXcLYaqvLlfssmu1DRxAJh5VMUKsbnpwVrvz+FKN0n9sbjEY7H5B8pkFPp
+1DOkNNeRu9r2zzrDRPhrXqfoujBIuLw1dVsfxpECgYEArOW3p14H4BIisYfqYzR4
+vEXU5w1MH6sTgAnGhTI7WgCYK/1R4rcjPA+iGGaj+QQ5IJUbpYT7r6v2aiNyge7d
+muhtUqXn1Hsox3O6cxerFlxd5OqO+p1O1Xuta4TP9Moh7ujRPHX1dUa41+3LHaku
+6ylIABtn5gBAm3OYvOwaSyw=
+-----END PRIVATE KEY-----
diff --git a/spec/ssl/pkcs8-server-key.pem b/spec/ssl/pkcs8-server-key.pem
new file mode 100644
index 0000000..c803fba
--- /dev/null
+++ b/spec/ssl/pkcs8-server-key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQCtyxWYD01zMQby
+RwzqzrGypf/x/rhNUT70HChE9TpojPmCe9e38eSvz1+0kGWb4VGKB705mPsY8yry
+IO/T2StZI11Ddh7qGbiXJFFeU3s4lrcis3qZTZNvui8hPVmGRjn6TRe6FkX0B/lF
+Ip0TV3X2aSAIU7oZpn6qmn4WZFFKgy/EVuDzeaf/RvTrfhnOhbBwhX+93WXt50fK
+73YFBEUJ4hOO0VoB34O/555OHY4/FRjTWTusL2LgxcTcE/GRGk9BPBN4cfhmlJHB
+hTSiXkGOyLGv8kbrYffockDhkiPJbMSdWIRV+ZpQX3YXf2y3lGOGbWptgMi3ttzh
+6RaKmoZJAgMBAAECggEAFhAyLZvDuVwABcH/Yc/bv1JTq+UqgKZP1627bwWy5JMB
+Gg+e0ztiTO+GtuWeAKwaLevNmgJR3lkAmryTtdFcL3TN4kKcqhuZ05ZIvjDa89Qu
+a7ldVxkCHq0ETrP7KZDAy4X9/SHWv6RDgQNj7ZCs6RtvdZ8rgRYh/oaeezlBGLRZ
+OIeWWHTYk8CMhu+tzR+BHZdXUEHo/sZHwmpdhQQ0HtXs87Eo51waVwHL0BgXT+ak
+v7pbhe6sGgNgoix59Lu9WbazxXIyRYDMkZfg0fCauRMG05Wvaeuonk/zlC88Eg3M
+yQIW8+Boe7M2yI+egRYeM2TCBnr2B7n8fTG/xQEKAQKBgQDe+hgYWboh6ejCarAB
+UOd90D//aLtbyu0HBQ6YNiPPUvVmEMfDVscxw6BXTtRWpHYSxsPV+IpUM73St/cu
+RmrP1DOTT4sJJf9lvNg35+OceGHRe0hVUU9mVqBMbLz5NOPwxWlq4eSgK+cAKq3g
+5lp47IFxX0R+g+dvk7YhSy4+rwKBgQDHiEBLjZahY100PffLygIwvwbTyoK9UMjS
+On1sGqOf9aF79zQIIRhEmPN/je9jqnAcf8+ivS1dLYoUboosm7215349Uw6S9C43
+RIn1iLZRGO7Beq3KOrdp4NFnh3QsgoH4jp1gp4w6QyBC2uYBdQXA9GpUR2+Y45BI
+KVSHQV4IhwKBgQDQLig48+04pK9QdVOGpwa7DKfzytDCzx+mIi6SJlogw4+ij6Ay
+3N51s/QMD+loS3yB41oMeFSOcRCVoHUDm3M2PyU4MFfbXsKpNjuZVsPH3w1VDAlo
+vtWm8tIPCKcW9S6sKWRXCjju4o52NWLKS8fEhuwD8bJ9fKGkJwEw7IRsuQKBgQCU
+D4feSI+I7InB9WXGI/1iHK49RJ2lS6fpUBu3t0DJtuSAb5x9l8lBRdoSQclsxJFy
+pGj4Erbx2JQIu0nu9hZdQA1OBi7fXzBYNJTGzQ60uPKaQaVqVg26FGhvEXVkfedi
+ALnJeiq1JRBwa6yXUjXVy8iHB4dJBTwQQBMIVronSwKBgQDOPYklqdzRv+u4XukW
+WsvK7GLj9huwdH0NI5gouGMb5OTgfVo66+urq4qYcHRN4vcdxbP9KoJPBvKhdx+D
+A36I2ERs//92gIz6lcNsqmdAzHAX1yzp4IyFGsjzaKibnQrft7hb9Kfg9VypBb/G
+31x7mm+gZm4RP3jbcBPDn0xU/w==
+-----END PRIVATE KEY-----
diff --git a/spec/ssl/server-cert.pem b/spec/ssl/server-cert.pem
new file mode 100644
index 0000000..9ec690d
--- /dev/null
+++ b/spec/ssl/server-cert.pem
@@ -0,0 +1,17 @@
+-----BEGIN CERTIFICATE-----
+MIICqzCCAZMCAQEwDQYJKoZIhvcNAQELBQAwFzEVMBMGA1UEAwwMY2FfbXlzcWwy
+Z2VtMB4XDTE1MDkwOTA0NTcyMVoXDTI1MDcxODA0NTcyMVowIDEeMBwGA1UEAwwV
+bXlzcWwyZ2VtLmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB
+CgKCAQEArcsVmA9NczEG8kcM6s6xsqX/8f64TVE+9BwoRPU6aIz5gnvXt/Hkr89f
+tJBlm+FRige9OZj7GPMq8iDv09krWSNdQ3Ye6hm4lyRRXlN7OJa3IrN6mU2Tb7ov
+IT1ZhkY5+k0XuhZF9Af5RSKdE1d19mkgCFO6GaZ+qpp+FmRRSoMvxFbg83mn/0b0
+634ZzoWwcIV/vd1l7edHyu92BQRFCeITjtFaAd+Dv+eeTh2OPxUY01k7rC9i4MXE
+3BPxkRpPQTwTeHH4ZpSRwYU0ol5Bjsixr/JG62H36HJA4ZIjyWzEnViEVfmaUF92
+F39st5Rjhm1qbYDIt7bc4ekWipqGSQIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBB
+PtRuVmOHiRPH3PmsZPtkgVagznojO+r0GDTxys5bxof8+HcokL6gbb4bRqzUQTdC
+98RTsuFPnd/I8FbbaL+UyeXeKjjOEBPFyllOdykmpd67mHCA89574y7Ib7lkvtr1
+nQFMbeKmcz4uLm1vAi/aOdAIA2de4yJU2XnOkVLDgYnQxpWR451WKqt/FtiuCzpQ
+E3peEemM2XVxvCMmfBAaroAyLYFrWOhNA7UoWVsubp7Ypf7SYuOh+sU6JLsYSadQ
+XVqgvIKG4deUpdl7oUBRz79spAi1OpHWiNmW3b+8nKJoHTfYkKzCebeLdI++xSjX
+jXNryv5xK88LFIPKL/7e
+-----END CERTIFICATE-----
diff --git a/spec/ssl/server-key.pem b/spec/ssl/server-key.pem
new file mode 100644
index 0000000..8bc4537
--- /dev/null
+++ b/spec/ssl/server-key.pem
@@ -0,0 +1,27 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIIEpQIBAAKCAQEArcsVmA9NczEG8kcM6s6xsqX/8f64TVE+9BwoRPU6aIz5gnvX
+t/Hkr89ftJBlm+FRige9OZj7GPMq8iDv09krWSNdQ3Ye6hm4lyRRXlN7OJa3IrN6
+mU2Tb7ovIT1ZhkY5+k0XuhZF9Af5RSKdE1d19mkgCFO6GaZ+qpp+FmRRSoMvxFbg
+83mn/0b0634ZzoWwcIV/vd1l7edHyu92BQRFCeITjtFaAd+Dv+eeTh2OPxUY01k7
+rC9i4MXE3BPxkRpPQTwTeHH4ZpSRwYU0ol5Bjsixr/JG62H36HJA4ZIjyWzEnViE
+VfmaUF92F39st5Rjhm1qbYDIt7bc4ekWipqGSQIDAQABAoIBABYQMi2bw7lcAAXB
+/2HP279SU6vlKoCmT9etu28FsuSTARoPntM7YkzvhrblngCsGi3rzZoCUd5ZAJq8
+k7XRXC90zeJCnKobmdOWSL4w2vPULmu5XVcZAh6tBE6z+ymQwMuF/f0h1r+kQ4ED
+Y+2QrOkbb3WfK4EWIf6Gnns5QRi0WTiHllh02JPAjIbvrc0fgR2XV1BB6P7GR8Jq
+XYUENB7V7POxKOdcGlcBy9AYF0/mpL+6W4XurBoDYKIsefS7vVm2s8VyMkWAzJGX
+4NHwmrkTBtOVr2nrqJ5P85QvPBINzMkCFvPgaHuzNsiPnoEWHjNkwgZ69ge5/H0x
+v8UBCgECgYEA3voYGFm6IenowmqwAVDnfdA//2i7W8rtBwUOmDYjz1L1ZhDHw1bH
+McOgV07UVqR2EsbD1fiKVDO90rf3LkZqz9Qzk0+LCSX/ZbzYN+fjnHhh0XtIVVFP
+ZlagTGy8+TTj8MVpauHkoCvnACqt4OZaeOyBcV9EfoPnb5O2IUsuPq8CgYEAx4hA
+S42WoWNdND33y8oCML8G08qCvVDI0jp9bBqjn/Whe/c0CCEYRJjzf43vY6pwHH/P
+or0tXS2KFG6KLJu9ted+PVMOkvQuN0SJ9Yi2URjuwXqtyjq3aeDRZ4d0LIKB+I6d
+YKeMOkMgQtrmAXUFwPRqVEdvmOOQSClUh0FeCIcCgYEA0C4oOPPtOKSvUHVThqcG
+uwyn88rQws8fpiIukiZaIMOPoo+gMtzedbP0DA/paEt8geNaDHhUjnEQlaB1A5tz
+Nj8lODBX217CqTY7mVbDx98NVQwJaL7VpvLSDwinFvUurClkVwo47uKOdjViykvH
+xIbsA/GyfXyhpCcBMOyEbLkCgYEAlA+H3kiPiOyJwfVlxiP9YhyuPUSdpUun6VAb
+t7dAybbkgG+cfZfJQUXaEkHJbMSRcqRo+BK28diUCLtJ7vYWXUANTgYu318wWDSU
+xs0OtLjymkGlalYNuhRobxF1ZH3nYgC5yXoqtSUQcGusl1I11cvIhweHSQU8EEAT
+CFa6J0sCgYEAzj2JJanc0b/ruF7pFlrLyuxi4/YbsHR9DSOYKLhjG+Tk4H1aOuvr
+q6uKmHB0TeL3HcWz/SqCTwbyoXcfgwN+iNhEbP//doCM+pXDbKpnQMxwF9cs6eCM
+hRrI82iom50K37e4W/Sn4PVcqQW/xt9ce5pvoGZuET9423ATw59MVP8=
+-----END RSA PRIVATE KEY-----
diff --git a/spec/ssl/server-req.pem b/spec/ssl/server-req.pem
new file mode 100644
index 0000000..9467550
--- /dev/null
+++ b/spec/ssl/server-req.pem
@@ -0,0 +1,15 @@
+-----BEGIN CERTIFICATE REQUEST-----
+MIICZTCCAU0CAQAwIDEeMBwGA1UEAwwVbXlzcWwyZ2VtLmV4YW1wbGUuY29tMIIB
+IjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArcsVmA9NczEG8kcM6s6xsqX/
+8f64TVE+9BwoRPU6aIz5gnvXt/Hkr89ftJBlm+FRige9OZj7GPMq8iDv09krWSNd
+Q3Ye6hm4lyRRXlN7OJa3IrN6mU2Tb7ovIT1ZhkY5+k0XuhZF9Af5RSKdE1d19mkg
+CFO6GaZ+qpp+FmRRSoMvxFbg83mn/0b0634ZzoWwcIV/vd1l7edHyu92BQRFCeIT
+jtFaAd+Dv+eeTh2OPxUY01k7rC9i4MXE3BPxkRpPQTwTeHH4ZpSRwYU0ol5Bjsix
+r/JG62H36HJA4ZIjyWzEnViEVfmaUF92F39st5Rjhm1qbYDIt7bc4ekWipqGSQID
+AQABoAAwDQYJKoZIhvcNAQELBQADggEBAInWIFsq14b8PhDroMMvi1ma30xyQGjg
+KObIxakwXkliSxmCdVqV4+MV6w8hK3z0q7H+NzRFByjo1PnU8BCIa058m5uvbjmM
+wGQvpcxmpm1p8VKKoeTqvE8OelbrqHrmyNIq7E/S3UZelVt+HOIPJOOs/aqEzaEg
+VL1u+4kCMbHM2rG8dii060oZ5i/gUtMn2TQWcNjSQBvaVztW5FOL74bYkoq0zIwd
+MFl2BoYyAnERJEcBmh1f+D7MuxPaqTUKjUmfDbHCMAAyTS1FHr9AnIN0/C2Mxywl
+H/zL9/DkfR53KZjITkf2gTH5D/N5oDUwmgCg6UZ0MeTOP27jvgcvb/k=
+-----END CERTIFICATE REQUEST-----
diff --git a/support/mysql_enc_to_ruby.rb b/support/mysql_enc_to_ruby.rb
index 4a3ef70..fbe1562 100644
--- a/support/mysql_enc_to_ruby.rb
+++ b/support/mysql_enc_to_ruby.rb
@@ -42,7 +42,7 @@ mysql_to_rb = {
   "binary"   => "ASCII-8BIT",
   "geostd8"  => "NULL",
   "cp932"    => "Windows-31J",
-  "eucjpms"  => "eucJP-ms"
+  "eucjpms"  => "eucJP-ms",
 }
 
 client     = Mysql2::Client.new(:username => user, :password => pass, :host => host, :port => port.to_i)
@@ -53,7 +53,7 @@ encodings_with_nil = Array.new(encodings.size)
 collations.each do |collation|
   mysql_col_idx = collation[2].to_i
   rb_enc = mysql_to_rb[collation[1]]
-  encodings[mysql_col_idx-1] = [mysql_col_idx, rb_enc]
+  encodings[mysql_col_idx - 1] = [mysql_col_idx, rb_enc]
 end
 
 encodings.each_with_index do |encoding, idx|
@@ -65,10 +65,10 @@ encodings_with_nil.sort! do |a, b|
 end
 
 encodings_with_nil = encodings_with_nil.map do |encoding|
-  name = "NULL"
-
-  if !encoding.nil? && encoding[1] != "NULL"
-    name = "\"#{encoding[1]}\""
+  name = if encoding.nil? || encoding[1] == 'NULL'
+    'NULL'
+  else
+    "\"#{encoding[1]}\""
   end
 
   "  #{name}"
@@ -76,7 +76,6 @@ end
 
 # start printing output
 
-puts "const char *mysql2_mysql_enc_to_rb[] = {"
+puts "static const char *mysql2_mysql_enc_to_rb[] = {"
 puts encodings_with_nil.join(",\n")
 puts "};"
-puts
diff --git a/support/ruby_enc_to_mysql.rb b/support/ruby_enc_to_mysql.rb
index 112016c..52603b3 100644
--- a/support/ruby_enc_to_mysql.rb
+++ b/support/ruby_enc_to_mysql.rb
@@ -37,7 +37,7 @@ mysql_to_rb = {
   "binary"   => "ASCII-8BIT",
   "geostd8"  => nil,
   "cp932"    => "Windows-31J",
-  "eucjpms"  => "eucJP-ms"
+  "eucjpms"  => "eucJP-ms",
 }
 
 puts <<-header

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-ruby-extras/ruby-mysql2.git



More information about the Pkg-ruby-extras-commits mailing list