mysql2 0.3.21 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -0
  3. data/README.md +63 -12
  4. data/examples/eventmachine.rb +1 -1
  5. data/examples/threaded.rb +4 -6
  6. data/ext/mysql2/client.c +100 -95
  7. data/ext/mysql2/client.h +21 -1
  8. data/ext/mysql2/extconf.rb +86 -43
  9. data/ext/mysql2/infile.c +2 -2
  10. data/ext/mysql2/mysql2_ext.c +1 -0
  11. data/ext/mysql2/mysql2_ext.h +5 -6
  12. data/ext/mysql2/mysql_enc_name_to_ruby.h +2 -2
  13. data/ext/mysql2/mysql_enc_to_ruby.h +25 -22
  14. data/ext/mysql2/result.c +469 -99
  15. data/ext/mysql2/result.h +11 -4
  16. data/ext/mysql2/statement.c +494 -0
  17. data/ext/mysql2/statement.h +19 -0
  18. data/lib/mysql2/client.rb +52 -22
  19. data/lib/mysql2/console.rb +1 -1
  20. data/lib/mysql2/em.rb +5 -6
  21. data/lib/mysql2/error.rb +17 -26
  22. data/lib/mysql2/field.rb +3 -0
  23. data/lib/mysql2/statement.rb +17 -0
  24. data/lib/mysql2/version.rb +1 -1
  25. data/lib/mysql2.rb +37 -35
  26. data/spec/em/em_spec.rb +21 -21
  27. data/spec/mysql2/client_spec.rb +340 -302
  28. data/spec/mysql2/error_spec.rb +37 -36
  29. data/spec/mysql2/result_spec.rb +200 -209
  30. data/spec/mysql2/statement_spec.rb +684 -0
  31. data/spec/spec_helper.rb +7 -0
  32. data/spec/ssl/ca-cert.pem +17 -0
  33. data/spec/ssl/ca-key.pem +27 -0
  34. data/spec/ssl/ca.cnf +22 -0
  35. data/spec/ssl/cert.cnf +22 -0
  36. data/spec/ssl/client-cert.pem +17 -0
  37. data/spec/ssl/client-key.pem +27 -0
  38. data/spec/ssl/client-req.pem +15 -0
  39. data/spec/ssl/gen_certs.sh +48 -0
  40. data/spec/ssl/pkcs8-client-key.pem +28 -0
  41. data/spec/ssl/pkcs8-server-key.pem +28 -0
  42. data/spec/ssl/server-cert.pem +17 -0
  43. data/spec/ssl/server-key.pem +27 -0
  44. data/spec/ssl/server-req.pem +15 -0
  45. data/support/mysql_enc_to_ruby.rb +7 -8
  46. data/support/ruby_enc_to_mysql.rb +1 -1
  47. metadata +41 -47
data/ext/mysql2/client.h CHANGED
@@ -50,7 +50,27 @@ typedef struct {
50
50
  MYSQL *client;
51
51
  } mysql_client_wrapper;
52
52
 
53
- void init_mysql2_client();
53
+ #define REQUIRE_CONNECTED(wrapper) \
54
+ REQUIRE_INITIALIZED(wrapper) \
55
+ if (!wrapper->connected && !wrapper->reconnect_enabled) { \
56
+ rb_raise(cMysql2Error, "closed MySQL connection"); \
57
+ }
58
+
59
+ void rb_mysql_client_set_active_thread(VALUE self);
60
+
61
+ #define MARK_CONN_INACTIVE(conn) do {\
62
+ wrapper->active_thread = Qnil; \
63
+ } while(0)
64
+
65
+ #define GET_CLIENT(self) \
66
+ mysql_client_wrapper *wrapper; \
67
+ Data_Get_Struct(self, mysql_client_wrapper, wrapper);
68
+
69
+ void init_mysql2_client(void);
54
70
  void decr_mysql2_client(mysql_client_wrapper *wrapper);
55
71
 
56
72
  #endif
73
+
74
+ #ifndef HAVE_RB_HASH_DUP
75
+ VALUE rb_hash_dup(VALUE other);
76
+ #endif
@@ -1,7 +1,8 @@
1
1
  # encoding: UTF-8
2
2
  require 'mkmf'
3
+ require 'English'
3
4
 
4
- def asplode lib
5
+ def asplode(lib)
5
6
  if RUBY_PLATFORM =~ /mingw|mswin/
6
7
  abort "-----\n#{lib} is missing. Check your installation of MySQL or Connector/C, and try again.\n-----"
7
8
  elsif RUBY_PLATFORM =~ /darwin/
@@ -22,7 +23,7 @@ have_func('rb_intern3')
22
23
 
23
24
  # borrowed from mysqlplus
24
25
  # http://github.com/oldmoe/mysqlplus/blob/master/ext/extconf.rb
25
- dirs = ENV['PATH'].split(File::PATH_SEPARATOR) + %w[
26
+ dirs = ENV.fetch('PATH').split(File::PATH_SEPARATOR) + %w(
26
27
  /opt
27
28
  /opt/local
28
29
  /opt/local/mysql
@@ -33,13 +34,15 @@ dirs = ENV['PATH'].split(File::PATH_SEPARATOR) + %w[
33
34
  /usr/local/mysql
34
35
  /usr/local/mysql-*
35
36
  /usr/local/lib/mysql5*
36
- ].map{|dir| "#{dir}/bin" }
37
+ /usr/local/opt/mysql5*
38
+ ).map { |dir| dir << '/bin' }
37
39
 
38
40
  GLOB = "{#{dirs.join(',')}}/{mysql_config,mysql_config5,mariadb_config}"
39
41
 
40
42
  # If the user has provided a --with-mysql-dir argument, we must respect it or fail.
41
43
  inc, lib = dir_config('mysql')
42
44
  if inc && lib
45
+ # TODO: Remove when 2.0.0 is the minimum supported version
43
46
  # Ruby versions not incorporating the mkmf fix at
44
47
  # https://bugs.ruby-lang.org/projects/ruby-trunk/repository/revisions/39717
45
48
  # do not properly search for lib directories, and must be corrected
@@ -47,48 +50,33 @@ if inc && lib
47
50
  @libdir_basename = 'lib'
48
51
  inc, lib = dir_config('mysql')
49
52
  end
50
- abort "-----\nCannot find include dir(s) #{inc}\n-----" unless inc && inc.split(File::PATH_SEPARATOR).any?{|dir| File.directory?(dir)}
51
- abort "-----\nCannot find library dir(s) #{lib}\n-----" unless lib && lib.split(File::PATH_SEPARATOR).any?{|dir| File.directory?(dir)}
52
- warn "-----\nUsing --with-mysql-dir=#{File.dirname inc}\n-----"
53
+ abort "-----\nCannot find include dir(s) #{inc}\n-----" unless inc && inc.split(File::PATH_SEPARATOR).any? { |dir| File.directory?(dir) }
54
+ abort "-----\nCannot find library dir(s) #{lib}\n-----" unless lib && lib.split(File::PATH_SEPARATOR).any? { |dir| File.directory?(dir) }
55
+ warn "-----\nUsing --with-mysql-dir=#{File.dirname inc}\n-----"
53
56
  rpath_dir = lib
54
- elsif mc = (with_config('mysql-config') || Dir[GLOB].first)
57
+ elsif (mc = (with_config('mysql-config') || Dir[GLOB].first))
55
58
  # If the user has provided a --with-mysql-config argument, we must respect it or fail.
56
59
  # If the user gave --with-mysql-config with no argument means we should try to find it.
57
60
  mc = Dir[GLOB].first if mc == true
58
- abort "-----\nCannot find mysql_config at #{mc}\n-----" unless mc && File.exists?(mc)
61
+ abort "-----\nCannot find mysql_config at #{mc}\n-----" unless mc && File.exist?(mc)
59
62
  abort "-----\nCannot execute mysql_config at #{mc}\n-----" unless File.executable?(mc)
60
- warn "-----\nUsing mysql_config at #{mc}\n-----"
63
+ warn "-----\nUsing mysql_config at #{mc}\n-----"
61
64
  ver = `#{mc} --version`.chomp.to_f
62
65
  includes = `#{mc} --include`.chomp
63
- exit 1 if $? != 0
66
+ abort unless $CHILD_STATUS.success?
64
67
  libs = `#{mc} --libs_r`.chomp
65
68
  # MySQL 5.5 and above already have re-entrant code in libmysqlclient (no _r).
66
- if ver >= 5.5 || libs.empty?
67
- libs = `#{mc} --libs`.chomp
68
- end
69
- exit 1 if $? != 0
69
+ libs = `#{mc} --libs`.chomp if ver >= 5.5 || libs.empty?
70
+ abort unless $CHILD_STATUS.success?
70
71
  $INCFLAGS += ' ' + includes
71
72
  $libs = libs + " " + $libs
72
73
  rpath_dir = libs
73
74
  else
74
- inc, lib = dir_config('mysql', '/usr/local')
75
- unless find_library('mysqlclient', 'mysql_query', lib, "#{lib}/mysql")
76
- found = false
77
- # For some systems and some versions of libmysqlclient, there were extra
78
- # libraries needed to link. Try each typical extra library, add it to the
79
- # global compile flags, and see if that allows us to link libmysqlclient.
80
- warn "-----\nlibmysqlclient is missing. Trying again with extra runtime libraries...\n-----"
81
-
82
- %w{ m z socket nsl mygcc }.each do |extra_lib|
83
- if have_library(extra_lib) && find_library('mysqlclient', 'mysql_query', lib, "#{lib}/mysql")
84
- found = true
85
- break
86
- end
87
- end
88
- asplode('libmysqlclient') unless found
89
- end
75
+ _, usr_local_lib = dir_config('mysql', '/usr/local')
90
76
 
91
- rpath_dir = lib
77
+ asplode("mysql client") unless find_library('mysqlclient', 'mysql_query', usr_local_lib, "#{usr_local_lib}/mysql")
78
+
79
+ rpath_dir = usr_local_lib
92
80
  end
93
81
 
94
82
  if have_header('mysql.h')
@@ -99,16 +87,71 @@ else
99
87
  asplode 'mysql.h'
100
88
  end
101
89
 
102
- %w{ errmsg.h mysqld_error.h }.each do |h|
90
+ %w(errmsg.h mysqld_error.h).each do |h|
103
91
  header = [prefix, h].compact.join '/'
104
- asplode h unless have_header h
92
+ asplode h unless have_header header
93
+ end
94
+
95
+ # This is our wishlist. We use whichever flags work on the host.
96
+ # -Wall and -Wextra are included by default.
97
+ wishlist = [
98
+ '-Weverything',
99
+ '-Wno-bad-function-cast', # rb_thread_call_without_gvl returns void * that we cast to VALUE
100
+ '-Wno-conditional-uninitialized', # false positive in client.c
101
+ '-Wno-covered-switch-default', # result.c -- enum_field_types (when fully covered, e.g. mysql 5.5)
102
+ '-Wno-declaration-after-statement', # GET_CLIENT followed by GET_STATEMENT in statement.c
103
+ '-Wno-disabled-macro-expansion', # rubby :(
104
+ '-Wno-documentation-unknown-command', # rubby :(
105
+ '-Wno-missing-field-initializers', # gperf generates bad code
106
+ '-Wno-missing-variable-declarations', # missing symbols due to ruby native ext initialization
107
+ '-Wno-padded', # mysql :(
108
+ '-Wno-reserved-id-macro', # rubby :(
109
+ '-Wno-sign-conversion', # gperf generates bad code
110
+ '-Wno-static-in-inline', # gperf generates bad code
111
+ '-Wno-switch-enum', # result.c -- enum_field_types (when not fully covered, e.g. mysql 5.6+)
112
+ '-Wno-undef', # rubinius :(
113
+ '-Wno-unreachable-code', # rubby :(
114
+ '-Wno-used-but-marked-unused', # rubby :(
115
+ ]
116
+
117
+ usable_flags = wishlist.select do |flag|
118
+ try_link('int main() {return 0;}', "-Werror #{flag}")
105
119
  end
106
120
 
107
- # These gcc style flags are also supported by clang and xcode compilers,
108
- # so we'll use a does-it-work test instead of an is-it-gcc test.
109
- gcc_flags = ' -Wall -funroll-loops'
110
- if try_link('int main() {return 0;}', gcc_flags)
111
- $CFLAGS << gcc_flags
121
+ $CFLAGS << ' ' << usable_flags.join(' ')
122
+
123
+ enabled_sanitizers = disabled_sanitizers = []
124
+ # Specify a commna-separated list of sanitizers, or try them all by default
125
+ sanitizers = with_config('sanitize')
126
+ case sanitizers
127
+ when true
128
+ # Try them all, turn on whatever we can
129
+ enabled_sanitizers = %w(address cfi integer memory thread undefined).select do |s|
130
+ try_link('int main() {return 0;}', "-Werror -fsanitize=#{s}")
131
+ end
132
+ abort "-----\nCould not enable any sanitizers!\n-----" if enabled_sanitizers.empty?
133
+ when String
134
+ # Figure out which sanitizers are supported
135
+ enabled_sanitizers, disabled_sanitizers = sanitizers.split(',').partition do |s|
136
+ try_link('int main() {return 0;}', "-Werror -fsanitize=#{s}")
137
+ end
138
+ end
139
+
140
+ unless disabled_sanitizers.empty?
141
+ abort "-----\nCould not enable requested sanitizers: #{disabled_sanitizers.join(',')}\n-----"
142
+ end
143
+
144
+ unless enabled_sanitizers.empty?
145
+ warn "-----\nEnabling sanitizers: #{enabled_sanitizers.join(',')}\n-----"
146
+ enabled_sanitizers.each do |s|
147
+ # address sanitizer requires runtime support
148
+ if s == 'address' # rubocop:disable Style/IfUnlessModifier
149
+ have_library('asan') || $LDFLAGS << ' -fsanitize=address'
150
+ end
151
+ $CFLAGS << " -fsanitize=#{s}"
152
+ end
153
+ # Options for line numbers in backtraces
154
+ $CFLAGS << ' -g -fno-omit-frame-pointer'
112
155
  end
113
156
 
114
157
  if RUBY_PLATFORM =~ /mswin|mingw/
@@ -119,7 +162,7 @@ if RUBY_PLATFORM =~ /mswin|mingw/
119
162
  # Use rake to rebuild only if these files change
120
163
  deffile = File.expand_path('../../../support/libmysql.def', __FILE__)
121
164
  libfile = File.expand_path(File.join(rpath_dir, 'libmysql.lib'))
122
- file 'libmysql.a' => [deffile, libfile] do |t|
165
+ file 'libmysql.a' => [deffile, libfile] do
123
166
  when_writing 'building libmysql.a' do
124
167
  # Ruby kindly shows us where dllwrap is, but that tool does more than we want.
125
168
  # Maybe in the future Ruby could provide RbConfig::CONFIG['DLLTOOL'] directly.
@@ -136,8 +179,8 @@ if RUBY_PLATFORM =~ /mswin|mingw/
136
179
 
137
180
  # Make sure the generated interface library works (if cross-compiling, trust without verifying)
138
181
  unless RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
139
- abort "-----\nCannot find libmysql.a\n----" unless have_library('libmysql')
140
- abort "-----\nCannot link to libmysql.a (my_init)\n----" unless have_func('my_init')
182
+ abort "-----\nCannot find libmysql.a\n-----" unless have_library('libmysql')
183
+ abort "-----\nCannot link to libmysql.a (my_init)\n-----" unless have_func('my_init')
141
184
  end
142
185
 
143
186
  # Vendor libmysql.dll
@@ -146,7 +189,7 @@ if RUBY_PLATFORM =~ /mswin|mingw/
146
189
 
147
190
  vendordll = File.join(vendordir, 'libmysql.dll')
148
191
  dllfile = File.expand_path(File.join(rpath_dir, 'libmysql.dll'))
149
- file vendordll => [dllfile, vendordir] do |t|
192
+ file vendordll => [dllfile, vendordir] do
150
193
  when_writing 'copying libmysql.dll' do
151
194
  cp dllfile, vendordll
152
195
  end
@@ -172,7 +215,7 @@ else
172
215
  warn "-----\nSetting mysql rpath to #{explicit_rpath}\n-----"
173
216
  $LDFLAGS << rpath_flags
174
217
  else
175
- if libdir = rpath_dir[%r{(-L)?(/[^ ]+)}, 2]
218
+ if (libdir = rpath_dir[%r{(-L)?(/[^ ]+)}, 2])
176
219
  rpath_flags = " -Wl,-rpath,#{libdir}"
177
220
  if RbConfig::CONFIG["RPATHFLAG"].to_s.empty? && try_link('int main() {return 0;}', rpath_flags)
178
221
  # Usually Ruby sets RPATHFLAG the right way for each system, but not on OS X.
data/ext/mysql2/infile.c CHANGED
@@ -56,7 +56,7 @@ mysql2_local_infile_init(void **ptr, const char *filename, void *userdata)
56
56
  * < 0 error
57
57
  */
58
58
  static int
59
- mysql2_local_infile_read(void *ptr, char *buf, uint buf_len)
59
+ mysql2_local_infile_read(void *ptr, char *buf, unsigned int buf_len)
60
60
  {
61
61
  int count;
62
62
  mysql2_local_infile_data *data = (mysql2_local_infile_data *)ptr;
@@ -95,7 +95,7 @@ mysql2_local_infile_end(void *ptr)
95
95
  * Error message number (see http://dev.mysql.com/doc/refman/5.0/en/error-messages-client.html)
96
96
  */
97
97
  static int
98
- mysql2_local_infile_error(void *ptr, char *error_msg, uint error_msg_len)
98
+ mysql2_local_infile_error(void *ptr, char *error_msg, unsigned int error_msg_len)
99
99
  {
100
100
  mysql2_local_infile_data *data = (mysql2_local_infile_data *) ptr;
101
101
 
@@ -9,4 +9,5 @@ void Init_mysql2() {
9
9
 
10
10
  init_mysql2_client();
11
11
  init_mysql2_result();
12
+ init_mysql2_statement();
12
13
  }
@@ -1,18 +1,14 @@
1
1
  #ifndef MYSQL2_EXT
2
2
  #define MYSQL2_EXT
3
3
 
4
+ void Init_mysql2(void);
5
+
4
6
  /* tell rbx not to use it's caching compat layer
5
7
  by doing this we're making a promise to RBX that
6
8
  we'll never modify the pointers we get back from RSTRING_PTR */
7
9
  #define RSTRING_NOT_MODIFIED
8
10
  #include <ruby.h>
9
11
 
10
- #ifndef HAVE_UINT
11
- #define HAVE_UINT
12
- typedef unsigned short ushort;
13
- typedef unsigned int uint;
14
- #endif
15
-
16
12
  #ifdef HAVE_MYSQL_H
17
13
  #include <mysql.h>
18
14
  #include <mysql_com.h>
@@ -33,12 +29,15 @@ typedef unsigned int uint;
33
29
  #endif
34
30
 
35
31
  #if defined(__GNUC__) && (__GNUC__ >= 3)
32
+ #define RB_MYSQL_NORETURN __attribute__ ((noreturn))
36
33
  #define RB_MYSQL_UNUSED __attribute__ ((unused))
37
34
  #else
35
+ #define RB_MYSQL_NORETURN
38
36
  #define RB_MYSQL_UNUSED
39
37
  #endif
40
38
 
41
39
  #include <client.h>
40
+ #include <statement.h>
42
41
  #include <result.h>
43
42
  #include <infile.h>
44
43
 
@@ -1,4 +1,4 @@
1
- /* C code produced by gperf version 3.0.3 */
1
+ /* C code produced by gperf version 3.0.4 */
2
2
  /* Command-line: gperf */
3
3
  /* Computed positions: -k'1,3,$' */
4
4
 
@@ -78,7 +78,7 @@ mysql2_mysql_enc_name_to_rb_hash (str, len)
78
78
 
79
79
  #ifdef __GNUC__
80
80
  __inline
81
- #ifdef __GNUC_STDC_INLINE__
81
+ #if defined __GNUC_STDC_INLINE__ || defined __GNUC_GNU_INLINE__
82
82
  __attribute__ ((__gnu_inline__))
83
83
  #endif
84
84
  #endif
@@ -1,4 +1,4 @@
1
- const char *mysql2_mysql_enc_to_rb[] = {
1
+ static const char *mysql2_mysql_enc_to_rb[] = {
2
2
  "Big5",
3
3
  "ISO-8859-2",
4
4
  NULL,
@@ -54,13 +54,13 @@ const char *mysql2_mysql_enc_to_rb[] = {
54
54
  "macRoman",
55
55
  "UTF-16",
56
56
  "UTF-16",
57
- NULL,
57
+ "",
58
58
  "Windows-1256",
59
59
  "Windows-1257",
60
60
  "Windows-1257",
61
61
  "UTF-32",
62
62
  "UTF-32",
63
- NULL,
63
+ "",
64
64
  "ASCII-8BIT",
65
65
  NULL,
66
66
  "US-ASCII",
@@ -119,10 +119,10 @@ const char *mysql2_mysql_enc_to_rb[] = {
119
119
  "UTF-16",
120
120
  "UTF-16",
121
121
  "UTF-16",
122
- NULL,
123
- NULL,
124
- NULL,
125
- NULL,
122
+ "UTF-16",
123
+ "UTF-16",
124
+ "UTF-16",
125
+ "UTF-16",
126
126
  NULL,
127
127
  NULL,
128
128
  NULL,
@@ -146,6 +146,10 @@ const char *mysql2_mysql_enc_to_rb[] = {
146
146
  "UTF-16BE",
147
147
  "UTF-16BE",
148
148
  "UTF-16BE",
149
+ "UTF-16BE",
150
+ "UTF-16BE",
151
+ "UTF-16BE",
152
+ "UTF-16BE",
149
153
  NULL,
150
154
  NULL,
151
155
  NULL,
@@ -153,11 +157,11 @@ const char *mysql2_mysql_enc_to_rb[] = {
153
157
  NULL,
154
158
  NULL,
155
159
  NULL,
156
- NULL,
157
- NULL,
158
- NULL,
159
- NULL,
160
- NULL,
160
+ "UTF-16BE",
161
+ "UTF-32",
162
+ "UTF-32",
163
+ "UTF-32",
164
+ "UTF-32",
161
165
  "UTF-32",
162
166
  "UTF-32",
163
167
  "UTF-32",
@@ -178,10 +182,6 @@ const char *mysql2_mysql_enc_to_rb[] = {
178
182
  "UTF-32",
179
183
  "UTF-32",
180
184
  "UTF-32",
181
- NULL,
182
- NULL,
183
- NULL,
184
- NULL,
185
185
  NULL,
186
186
  NULL,
187
187
  NULL,
@@ -210,6 +210,10 @@ const char *mysql2_mysql_enc_to_rb[] = {
210
210
  "UTF-8",
211
211
  "UTF-8",
212
212
  "UTF-8",
213
+ "UTF-8",
214
+ "UTF-8",
215
+ "UTF-8",
216
+ "UTF-8",
213
217
  NULL,
214
218
  NULL,
215
219
  NULL,
@@ -217,11 +221,11 @@ const char *mysql2_mysql_enc_to_rb[] = {
217
221
  NULL,
218
222
  NULL,
219
223
  NULL,
220
- NULL,
221
- NULL,
222
- NULL,
223
- NULL,
224
- NULL,
224
+ "UTF-8",
225
+ "UTF-8",
226
+ "UTF-8",
227
+ "UTF-8",
228
+ "UTF-8",
225
229
  "UTF-8",
226
230
  "UTF-8",
227
231
  "UTF-8",
@@ -243,4 +247,3 @@ const char *mysql2_mysql_enc_to_rb[] = {
243
247
  "UTF-8",
244
248
  "UTF-8"
245
249
  };
246
-
data/ext/mysql2/result.c CHANGED
@@ -1,7 +1,5 @@
1
1
  #include <mysql2_ext.h>
2
2
 
3
- #include <stdint.h>
4
-
5
3
  #include "mysql_enc_to_ruby.h"
6
4
 
7
5
  #ifdef HAVE_RUBY_ENCODING_H
@@ -50,12 +48,24 @@ static rb_encoding *binaryEncoding;
50
48
  #define MYSQL2_MIN_TIME 62171150401ULL
51
49
  #endif
52
50
 
53
- #define GET_RESULT(obj) \
51
+ #define GET_RESULT(self) \
54
52
  mysql2_result_wrapper *wrapper; \
55
53
  Data_Get_Struct(self, mysql2_result_wrapper, wrapper);
56
54
 
55
+ typedef struct {
56
+ int symbolizeKeys;
57
+ int asArray;
58
+ int castBool;
59
+ int cacheRows;
60
+ int cast;
61
+ int streaming;
62
+ ID db_timezone;
63
+ ID app_timezone;
64
+ VALUE block_given;
65
+ } result_each_args;
66
+
67
+ VALUE cBigDecimal, cDateTime, cDate;
57
68
  static VALUE cMysql2Result;
58
- static VALUE cBigDecimal, cDate, cDateTime;
59
69
  static VALUE opt_decimal_zero, opt_float_zero, opt_time_year, opt_time_month, opt_utc_offset;
60
70
  extern VALUE mMysql2, cMysql2Client, cMysql2Error;
61
71
  static ID intern_new, intern_utc, intern_local, intern_localtime, intern_local_offset, intern_civil, intern_new_offset;
@@ -63,6 +73,7 @@ static VALUE sym_symbolize_keys, sym_as, sym_array, sym_database_timezone, sym_a
63
73
  sym_local, sym_utc, sym_cast_booleans, sym_cache_rows, sym_cast, sym_stream, sym_name;
64
74
  static ID intern_merge;
65
75
 
76
+ /* Mark any VALUEs that are only referenced in C, so the GC won't get them. */
66
77
  static void rb_mysql_result_mark(void * wrapper) {
67
78
  mysql2_result_wrapper * w = wrapper;
68
79
  if (w) {
@@ -70,13 +81,46 @@ static void rb_mysql_result_mark(void * wrapper) {
70
81
  rb_gc_mark(w->rows);
71
82
  rb_gc_mark(w->encoding);
72
83
  rb_gc_mark(w->client);
84
+ rb_gc_mark(w->statement);
73
85
  }
74
86
  }
75
87
 
76
88
  /* this may be called manually or during GC */
77
89
  static void rb_mysql_result_free_result(mysql2_result_wrapper * wrapper) {
78
- if (wrapper && wrapper->resultFreed != 1) {
90
+ if (!wrapper) return;
91
+
92
+ if (wrapper->resultFreed != 1) {
93
+ if (wrapper->stmt_wrapper) {
94
+ if (!wrapper->stmt_wrapper->closed) {
95
+ mysql_stmt_free_result(wrapper->stmt_wrapper->stmt);
96
+
97
+ /* MySQL BUG? If the statement handle was previously used, and so
98
+ * mysql_stmt_bind_result was called, and if that result set and bind buffers were freed,
99
+ * MySQL still thinks the result set buffer is available and will prefetch the
100
+ * first result in mysql_stmt_execute. This will corrupt or crash the program.
101
+ * By setting bind_result_done back to 0, we make MySQL think that a result set
102
+ * has never been bound to this statement handle before to prevent the prefetch.
103
+ */
104
+ wrapper->stmt_wrapper->stmt->bind_result_done = 0;
105
+ }
106
+
107
+ if (wrapper->result_buffers) {
108
+ unsigned int i;
109
+ for (i = 0; i < wrapper->numberOfFields; i++) {
110
+ if (wrapper->result_buffers[i].buffer) {
111
+ xfree(wrapper->result_buffers[i].buffer);
112
+ }
113
+ }
114
+ xfree(wrapper->result_buffers);
115
+ xfree(wrapper->is_null);
116
+ xfree(wrapper->error);
117
+ xfree(wrapper->length);
118
+ }
119
+ /* Clue that the next statement execute will need to allocate a new result buffer. */
120
+ wrapper->result_buffers = NULL;
121
+ }
79
122
  /* FIXME: this may call flush_use_result, which can hit the socket */
123
+ /* For prepared statements, wrapper->result is the result metadata */
80
124
  mysql_free_result(wrapper->result);
81
125
  wrapper->resultFreed = 1;
82
126
  }
@@ -84,7 +128,7 @@ static void rb_mysql_result_free_result(mysql2_result_wrapper * wrapper) {
84
128
 
85
129
  /* this is called during GC */
86
130
  static void rb_mysql_result_free(void *ptr) {
87
- mysql2_result_wrapper * wrapper = ptr;
131
+ mysql2_result_wrapper *wrapper = ptr;
88
132
  rb_mysql_result_free_result(wrapper);
89
133
 
90
134
  // If the GC gets to client first it will be nil
@@ -92,6 +136,10 @@ static void rb_mysql_result_free(void *ptr) {
92
136
  decr_mysql2_client(wrapper->client_wrapper);
93
137
  }
94
138
 
139
+ if (wrapper->statement != Qnil) {
140
+ decr_mysql2_stmt(wrapper->stmt_wrapper);
141
+ }
142
+
95
143
  xfree(wrapper);
96
144
  }
97
145
 
@@ -106,7 +154,14 @@ static void *nogvl_fetch_row(void *ptr) {
106
154
  return mysql_fetch_row(result);
107
155
  }
108
156
 
109
- static VALUE rb_mysql_result_fetch_field(VALUE self, unsigned int idx, short int symbolize_keys) {
157
+ static void *nogvl_stmt_fetch(void *ptr) {
158
+ MYSQL_STMT *stmt = ptr;
159
+ uintptr_t r = mysql_stmt_fetch(stmt);
160
+
161
+ return (void *)r;
162
+ }
163
+
164
+ static VALUE rb_mysql_result_fetch_field(VALUE self, unsigned int idx, int symbolize_keys) {
110
165
  VALUE rb_field;
111
166
  GET_RESULT(self);
112
167
 
@@ -150,7 +205,7 @@ static VALUE rb_mysql_result_fetch_field(VALUE self, unsigned int idx, short int
150
205
 
151
206
  #ifdef HAVE_RUBY_ENCODING_H
152
207
  static VALUE mysql2_set_field_string_encoding(VALUE val, MYSQL_FIELD field, rb_encoding *default_internal_enc, rb_encoding *conn_enc) {
153
- /* if binary flag is set, respect it's wishes */
208
+ /* if binary flag is set, respect its wishes */
154
209
  if (field.flags & BINARY_FLAG && field.charsetnr == 63) {
155
210
  rb_enc_associate(val, binaryEncoding);
156
211
  } else if (!field.charsetnr) {
@@ -185,7 +240,7 @@ static VALUE mysql2_set_field_string_encoding(VALUE val, MYSQL_FIELD field, rb_e
185
240
  */
186
241
  static unsigned int msec_char_to_uint(char *msec_char, size_t len)
187
242
  {
188
- int i;
243
+ size_t i;
189
244
  for (i = 0; i < (len - 1); i++) {
190
245
  if (msec_char[i] == '\0') {
191
246
  msec_char[i] = '0';
@@ -194,7 +249,271 @@ static unsigned int msec_char_to_uint(char *msec_char, size_t len)
194
249
  return (unsigned int)strtoul(msec_char, NULL, 10);
195
250
  }
196
251
 
197
- 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) {
252
+ static void rb_mysql_result_alloc_result_buffers(VALUE self, MYSQL_FIELD *fields) {
253
+ unsigned int i;
254
+ GET_RESULT(self);
255
+
256
+ if (wrapper->result_buffers != NULL) return;
257
+
258
+ wrapper->result_buffers = xcalloc(wrapper->numberOfFields, sizeof(MYSQL_BIND));
259
+ wrapper->is_null = xcalloc(wrapper->numberOfFields, sizeof(my_bool));
260
+ wrapper->error = xcalloc(wrapper->numberOfFields, sizeof(my_bool));
261
+ wrapper->length = xcalloc(wrapper->numberOfFields, sizeof(unsigned long));
262
+
263
+ for (i = 0; i < wrapper->numberOfFields; i++) {
264
+ wrapper->result_buffers[i].buffer_type = fields[i].type;
265
+
266
+ // mysql type | C type
267
+ switch(fields[i].type) {
268
+ case MYSQL_TYPE_NULL: // NULL
269
+ break;
270
+ case MYSQL_TYPE_TINY: // signed char
271
+ wrapper->result_buffers[i].buffer = xcalloc(1, sizeof(signed char));
272
+ wrapper->result_buffers[i].buffer_length = sizeof(signed char);
273
+ break;
274
+ case MYSQL_TYPE_SHORT: // short int
275
+ wrapper->result_buffers[i].buffer = xcalloc(1, sizeof(short int));
276
+ wrapper->result_buffers[i].buffer_length = sizeof(short int);
277
+ break;
278
+ case MYSQL_TYPE_INT24: // int
279
+ case MYSQL_TYPE_LONG: // int
280
+ case MYSQL_TYPE_YEAR: // int
281
+ wrapper->result_buffers[i].buffer = xcalloc(1, sizeof(int));
282
+ wrapper->result_buffers[i].buffer_length = sizeof(int);
283
+ break;
284
+ case MYSQL_TYPE_LONGLONG: // long long int
285
+ wrapper->result_buffers[i].buffer = xcalloc(1, sizeof(long long int));
286
+ wrapper->result_buffers[i].buffer_length = sizeof(long long int);
287
+ break;
288
+ case MYSQL_TYPE_FLOAT: // float
289
+ case MYSQL_TYPE_DOUBLE: // double
290
+ wrapper->result_buffers[i].buffer = xcalloc(1, sizeof(double));
291
+ wrapper->result_buffers[i].buffer_length = sizeof(double);
292
+ break;
293
+ case MYSQL_TYPE_TIME: // MYSQL_TIME
294
+ case MYSQL_TYPE_DATE: // MYSQL_TIME
295
+ case MYSQL_TYPE_NEWDATE: // MYSQL_TIME
296
+ case MYSQL_TYPE_DATETIME: // MYSQL_TIME
297
+ case MYSQL_TYPE_TIMESTAMP: // MYSQL_TIME
298
+ wrapper->result_buffers[i].buffer = xcalloc(1, sizeof(MYSQL_TIME));
299
+ wrapper->result_buffers[i].buffer_length = sizeof(MYSQL_TIME);
300
+ break;
301
+ case MYSQL_TYPE_DECIMAL: // char[]
302
+ case MYSQL_TYPE_NEWDECIMAL: // char[]
303
+ case MYSQL_TYPE_STRING: // char[]
304
+ case MYSQL_TYPE_VAR_STRING: // char[]
305
+ case MYSQL_TYPE_VARCHAR: // char[]
306
+ case MYSQL_TYPE_TINY_BLOB: // char[]
307
+ case MYSQL_TYPE_BLOB: // char[]
308
+ case MYSQL_TYPE_MEDIUM_BLOB: // char[]
309
+ case MYSQL_TYPE_LONG_BLOB: // char[]
310
+ case MYSQL_TYPE_BIT: // char[]
311
+ case MYSQL_TYPE_SET: // char[]
312
+ case MYSQL_TYPE_ENUM: // char[]
313
+ case MYSQL_TYPE_GEOMETRY: // char[]
314
+ default:
315
+ wrapper->result_buffers[i].buffer = xmalloc(fields[i].max_length);
316
+ wrapper->result_buffers[i].buffer_length = fields[i].max_length;
317
+ break;
318
+ }
319
+
320
+ wrapper->result_buffers[i].is_null = &wrapper->is_null[i];
321
+ wrapper->result_buffers[i].length = &wrapper->length[i];
322
+ wrapper->result_buffers[i].error = &wrapper->error[i];
323
+ wrapper->result_buffers[i].is_unsigned = ((fields[i].flags & UNSIGNED_FLAG) != 0);
324
+ }
325
+ }
326
+
327
+ static VALUE rb_mysql_result_fetch_row_stmt(VALUE self, MYSQL_FIELD * fields, const result_each_args *args)
328
+ {
329
+ VALUE rowVal;
330
+ unsigned int i = 0;
331
+
332
+ #ifdef HAVE_RUBY_ENCODING_H
333
+ rb_encoding *default_internal_enc;
334
+ rb_encoding *conn_enc;
335
+ #endif
336
+ GET_RESULT(self);
337
+
338
+ #ifdef HAVE_RUBY_ENCODING_H
339
+ default_internal_enc = rb_default_internal_encoding();
340
+ conn_enc = rb_to_encoding(wrapper->encoding);
341
+ #endif
342
+
343
+ if (args->asArray) {
344
+ rowVal = rb_ary_new2(wrapper->numberOfFields);
345
+ } else {
346
+ rowVal = rb_hash_new();
347
+ }
348
+ if (wrapper->fields == Qnil) {
349
+ wrapper->numberOfFields = mysql_num_fields(wrapper->result);
350
+ wrapper->fields = rb_ary_new2(wrapper->numberOfFields);
351
+ }
352
+
353
+ if (wrapper->result_buffers == NULL) {
354
+ rb_mysql_result_alloc_result_buffers(self, fields);
355
+ }
356
+
357
+ if (mysql_stmt_bind_result(wrapper->stmt_wrapper->stmt, wrapper->result_buffers)) {
358
+ rb_raise_mysql2_stmt_error(wrapper->stmt_wrapper);
359
+ }
360
+
361
+ {
362
+ switch((uintptr_t)rb_thread_call_without_gvl(nogvl_stmt_fetch, wrapper->stmt_wrapper->stmt, RUBY_UBF_IO, 0)) {
363
+ case 0:
364
+ /* success */
365
+ break;
366
+
367
+ case 1:
368
+ /* error */
369
+ rb_raise_mysql2_stmt_error(wrapper->stmt_wrapper);
370
+
371
+ case MYSQL_NO_DATA:
372
+ /* no more row */
373
+ return Qnil;
374
+
375
+ case MYSQL_DATA_TRUNCATED:
376
+ rb_raise(cMysql2Error, "IMPLBUG: caught MYSQL_DATA_TRUNCATED. should not come here as buffer_length is set to fields[i].max_length.");
377
+ }
378
+ }
379
+
380
+ for (i = 0; i < wrapper->numberOfFields; i++) {
381
+ VALUE field = rb_mysql_result_fetch_field(self, i, args->symbolizeKeys);
382
+ VALUE val = Qnil;
383
+ MYSQL_TIME *ts;
384
+
385
+ if (wrapper->is_null[i]) {
386
+ val = Qnil;
387
+ } else {
388
+ const MYSQL_BIND* const result_buffer = &wrapper->result_buffers[i];
389
+
390
+ switch(result_buffer->buffer_type) {
391
+ case MYSQL_TYPE_TINY: // signed char
392
+ if (args->castBool && fields[i].length == 1) {
393
+ val = (*((unsigned char*)result_buffer->buffer) != 0) ? Qtrue : Qfalse;
394
+ break;
395
+ }
396
+ if (result_buffer->is_unsigned) {
397
+ val = UINT2NUM(*((unsigned char*)result_buffer->buffer));
398
+ } else {
399
+ val = INT2NUM(*((signed char*)result_buffer->buffer));
400
+ }
401
+ break;
402
+ case MYSQL_TYPE_SHORT: // short int
403
+ if (result_buffer->is_unsigned) {
404
+ val = UINT2NUM(*((unsigned short int*)result_buffer->buffer));
405
+ } else {
406
+ val = INT2NUM(*((short int*)result_buffer->buffer));
407
+ }
408
+ break;
409
+ case MYSQL_TYPE_INT24: // int
410
+ case MYSQL_TYPE_LONG: // int
411
+ case MYSQL_TYPE_YEAR: // int
412
+ if (result_buffer->is_unsigned) {
413
+ val = UINT2NUM(*((unsigned int*)result_buffer->buffer));
414
+ } else {
415
+ val = INT2NUM(*((int*)result_buffer->buffer));
416
+ }
417
+ break;
418
+ case MYSQL_TYPE_LONGLONG: // long long int
419
+ if (result_buffer->is_unsigned) {
420
+ val = ULL2NUM(*((unsigned long long int*)result_buffer->buffer));
421
+ } else {
422
+ val = LL2NUM(*((long long int*)result_buffer->buffer));
423
+ }
424
+ break;
425
+ case MYSQL_TYPE_FLOAT: // float
426
+ val = rb_float_new((double)(*((float*)result_buffer->buffer)));
427
+ break;
428
+ case MYSQL_TYPE_DOUBLE: // double
429
+ val = rb_float_new((double)(*((double*)result_buffer->buffer)));
430
+ break;
431
+ case MYSQL_TYPE_DATE: // MYSQL_TIME
432
+ case MYSQL_TYPE_NEWDATE: // MYSQL_TIME
433
+ ts = (MYSQL_TIME*)result_buffer->buffer;
434
+ val = rb_funcall(cDate, intern_new, 3, INT2NUM(ts->year), INT2NUM(ts->month), INT2NUM(ts->day));
435
+ break;
436
+ case MYSQL_TYPE_TIME: // MYSQL_TIME
437
+ ts = (MYSQL_TIME*)result_buffer->buffer;
438
+ 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));
439
+ if (!NIL_P(args->app_timezone)) {
440
+ if (args->app_timezone == intern_local) {
441
+ val = rb_funcall(val, intern_localtime, 0);
442
+ } else { // utc
443
+ val = rb_funcall(val, intern_utc, 0);
444
+ }
445
+ }
446
+ break;
447
+ case MYSQL_TYPE_DATETIME: // MYSQL_TIME
448
+ case MYSQL_TYPE_TIMESTAMP: { // MYSQL_TIME
449
+ uint64_t seconds;
450
+
451
+ ts = (MYSQL_TIME*)result_buffer->buffer;
452
+ seconds = (ts->year*31557600ULL) + (ts->month*2592000ULL) + (ts->day*86400ULL) + (ts->hour*3600ULL) + (ts->minute*60ULL) + ts->second;
453
+
454
+ if (seconds < MYSQL2_MIN_TIME || seconds > MYSQL2_MAX_TIME) { // use DateTime instead
455
+ VALUE offset = INT2NUM(0);
456
+ if (args->db_timezone == intern_local) {
457
+ offset = rb_funcall(cMysql2Client, intern_local_offset, 0);
458
+ }
459
+ 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);
460
+ if (!NIL_P(args->app_timezone)) {
461
+ if (args->app_timezone == intern_local) {
462
+ offset = rb_funcall(cMysql2Client, intern_local_offset, 0);
463
+ val = rb_funcall(val, intern_new_offset, 1, offset);
464
+ } else { // utc
465
+ val = rb_funcall(val, intern_new_offset, 1, opt_utc_offset);
466
+ }
467
+ }
468
+ } else {
469
+ 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));
470
+ if (!NIL_P(args->app_timezone)) {
471
+ if (args->app_timezone == intern_local) {
472
+ val = rb_funcall(val, intern_localtime, 0);
473
+ } else { // utc
474
+ val = rb_funcall(val, intern_utc, 0);
475
+ }
476
+ }
477
+ }
478
+ break;
479
+ }
480
+ case MYSQL_TYPE_DECIMAL: // char[]
481
+ case MYSQL_TYPE_NEWDECIMAL: // char[]
482
+ val = rb_funcall(cBigDecimal, intern_new, 1, rb_str_new(result_buffer->buffer, *(result_buffer->length)));
483
+ break;
484
+ case MYSQL_TYPE_STRING: // char[]
485
+ case MYSQL_TYPE_VAR_STRING: // char[]
486
+ case MYSQL_TYPE_VARCHAR: // char[]
487
+ case MYSQL_TYPE_TINY_BLOB: // char[]
488
+ case MYSQL_TYPE_BLOB: // char[]
489
+ case MYSQL_TYPE_MEDIUM_BLOB: // char[]
490
+ case MYSQL_TYPE_LONG_BLOB: // char[]
491
+ case MYSQL_TYPE_BIT: // char[]
492
+ case MYSQL_TYPE_SET: // char[]
493
+ case MYSQL_TYPE_ENUM: // char[]
494
+ case MYSQL_TYPE_GEOMETRY: // char[]
495
+ default:
496
+ val = rb_str_new(result_buffer->buffer, *(result_buffer->length));
497
+ #ifdef HAVE_RUBY_ENCODING_H
498
+ val = mysql2_set_field_string_encoding(val, fields[i], default_internal_enc, conn_enc);
499
+ #endif
500
+ break;
501
+ }
502
+ }
503
+
504
+ if (args->asArray) {
505
+ rb_ary_push(rowVal, val);
506
+ } else {
507
+ rb_hash_aset(rowVal, field, val);
508
+ }
509
+ }
510
+
511
+ return rowVal;
512
+ }
513
+
514
+
515
+ static VALUE rb_mysql_result_fetch_row(VALUE self, MYSQL_FIELD * fields, const result_each_args *args)
516
+ {
198
517
  VALUE rowVal;
199
518
  MYSQL_ROW row;
200
519
  unsigned int i = 0;
@@ -217,7 +536,7 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo
217
536
  return Qnil;
218
537
  }
219
538
 
220
- if (asArray) {
539
+ if (args->asArray) {
221
540
  rowVal = rb_ary_new2(wrapper->numberOfFields);
222
541
  } else {
223
542
  rowVal = rb_hash_new();
@@ -229,12 +548,12 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo
229
548
  }
230
549
 
231
550
  for (i = 0; i < wrapper->numberOfFields; i++) {
232
- VALUE field = rb_mysql_result_fetch_field(self, i, symbolizeKeys);
551
+ VALUE field = rb_mysql_result_fetch_field(self, i, args->symbolizeKeys);
233
552
  if (row[i]) {
234
553
  VALUE val = Qnil;
235
554
  enum enum_field_types type = fields[i].type;
236
555
 
237
- if (!cast) {
556
+ if (!args->cast) {
238
557
  if (type == MYSQL_TYPE_NULL) {
239
558
  val = Qnil;
240
559
  } else {
@@ -249,14 +568,14 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo
249
568
  val = Qnil;
250
569
  break;
251
570
  case MYSQL_TYPE_BIT: /* BIT field (MySQL 5.0.3 and up) */
252
- if (castBool && fields[i].length == 1) {
571
+ if (args->castBool && fields[i].length == 1) {
253
572
  val = *row[i] == 1 ? Qtrue : Qfalse;
254
573
  }else{
255
574
  val = rb_str_new(row[i], fieldLengths[i]);
256
575
  }
257
576
  break;
258
577
  case MYSQL_TYPE_TINY: /* TINYINT field */
259
- if (castBool && fields[i].length == 1) {
578
+ if (args->castBool && fields[i].length == 1) {
260
579
  val = *row[i] != '0' ? Qtrue : Qfalse;
261
580
  break;
262
581
  }
@@ -299,9 +618,9 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo
299
618
  break;
300
619
  }
301
620
  msec = msec_char_to_uint(msec_char, sizeof(msec_char));
302
- 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));
303
- if (!NIL_P(app_timezone)) {
304
- if (app_timezone == intern_local) {
621
+ 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));
622
+ if (!NIL_P(args->app_timezone)) {
623
+ if (args->app_timezone == intern_local) {
305
624
  val = rb_funcall(val, intern_localtime, 0);
306
625
  } else { /* utc */
307
626
  val = rb_funcall(val, intern_utc, 0);
@@ -332,12 +651,12 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo
332
651
  } else {
333
652
  if (seconds < MYSQL2_MIN_TIME || seconds > MYSQL2_MAX_TIME) { /* use DateTime for larger date range, does not support microseconds */
334
653
  VALUE offset = INT2NUM(0);
335
- if (db_timezone == intern_local) {
654
+ if (args->db_timezone == intern_local) {
336
655
  offset = rb_funcall(cMysql2Client, intern_local_offset, 0);
337
656
  }
338
657
  val = rb_funcall(cDateTime, intern_civil, 7, UINT2NUM(year), UINT2NUM(month), UINT2NUM(day), UINT2NUM(hour), UINT2NUM(min), UINT2NUM(sec), offset);
339
- if (!NIL_P(app_timezone)) {
340
- if (app_timezone == intern_local) {
658
+ if (!NIL_P(args->app_timezone)) {
659
+ if (args->app_timezone == intern_local) {
341
660
  offset = rb_funcall(cMysql2Client, intern_local_offset, 0);
342
661
  val = rb_funcall(val, intern_new_offset, 1, offset);
343
662
  } else { /* utc */
@@ -346,9 +665,9 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo
346
665
  }
347
666
  } else {
348
667
  msec = msec_char_to_uint(msec_char, sizeof(msec_char));
349
- val = rb_funcall(rb_cTime, db_timezone, 7, UINT2NUM(year), UINT2NUM(month), UINT2NUM(day), UINT2NUM(hour), UINT2NUM(min), UINT2NUM(sec), UINT2NUM(msec));
350
- if (!NIL_P(app_timezone)) {
351
- if (app_timezone == intern_local) {
668
+ val = rb_funcall(rb_cTime, args->db_timezone, 7, UINT2NUM(year), UINT2NUM(month), UINT2NUM(day), UINT2NUM(hour), UINT2NUM(min), UINT2NUM(sec), UINT2NUM(msec));
669
+ if (!NIL_P(args->app_timezone)) {
670
+ if (args->app_timezone == intern_local) {
352
671
  val = rb_funcall(val, intern_localtime, 0);
353
672
  } else { /* utc */
354
673
  val = rb_funcall(val, intern_utc, 0);
@@ -398,13 +717,13 @@ static VALUE rb_mysql_result_fetch_row(VALUE self, ID db_timezone, ID app_timezo
398
717
  break;
399
718
  }
400
719
  }
401
- if (asArray) {
720
+ if (args->asArray) {
402
721
  rb_ary_push(rowVal, val);
403
722
  } else {
404
723
  rb_hash_aset(rowVal, field, val);
405
724
  }
406
725
  } else {
407
- if (asArray) {
726
+ if (args->asArray) {
408
727
  rb_ary_push(rowVal, Qnil);
409
728
  } else {
410
729
  rb_hash_aset(rowVal, field, Qnil);
@@ -432,7 +751,7 @@ static VALUE rb_mysql_result_fetch_fields(VALUE self) {
432
751
  wrapper->fields = rb_ary_new2(wrapper->numberOfFields);
433
752
  }
434
753
 
435
- if (RARRAY_LEN(wrapper->fields) != wrapper->numberOfFields) {
754
+ if ((my_ulonglong)RARRAY_LEN(wrapper->fields) != wrapper->numberOfFields) {
436
755
  for (i=0; i<wrapper->numberOfFields; i++) {
437
756
  rb_mysql_result_fetch_field(self, i, symbolizeKeys);
438
757
  }
@@ -441,55 +760,16 @@ static VALUE rb_mysql_result_fetch_fields(VALUE self) {
441
760
  return wrapper->fields;
442
761
  }
443
762
 
444
- static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
445
- VALUE defaults, opts, block;
446
- ID db_timezone, app_timezone, dbTz, appTz;
763
+ static VALUE rb_mysql_result_each_(VALUE self,
764
+ VALUE(*fetch_row_func)(VALUE, MYSQL_FIELD *fields, const result_each_args *args),
765
+ const result_each_args *args)
766
+ {
447
767
  unsigned long i;
448
- const char * errstr;
449
- int symbolizeKeys, asArray, castBool, cacheRows, cast;
450
- MYSQL_FIELD * fields = NULL;
768
+ const char *errstr;
769
+ MYSQL_FIELD *fields = NULL;
451
770
 
452
771
  GET_RESULT(self);
453
772
 
454
- defaults = rb_iv_get(self, "@query_options");
455
- Check_Type(defaults, T_HASH);
456
- if (rb_scan_args(argc, argv, "01&", &opts, &block) == 1) {
457
- opts = rb_funcall(defaults, intern_merge, 1, opts);
458
- } else {
459
- opts = defaults;
460
- }
461
-
462
- symbolizeKeys = RTEST(rb_hash_aref(opts, sym_symbolize_keys));
463
- asArray = rb_hash_aref(opts, sym_as) == sym_array;
464
- castBool = RTEST(rb_hash_aref(opts, sym_cast_booleans));
465
- cacheRows = RTEST(rb_hash_aref(opts, sym_cache_rows));
466
- cast = RTEST(rb_hash_aref(opts, sym_cast));
467
-
468
- if (wrapper->is_streaming && cacheRows) {
469
- rb_warn(":cache_rows is ignored if :stream is true");
470
- }
471
-
472
- dbTz = rb_hash_aref(opts, sym_database_timezone);
473
- if (dbTz == sym_local) {
474
- db_timezone = intern_local;
475
- } else if (dbTz == sym_utc) {
476
- db_timezone = intern_utc;
477
- } else {
478
- if (!NIL_P(dbTz)) {
479
- rb_warn(":database_timezone option must be :utc or :local - defaulting to :local");
480
- }
481
- db_timezone = intern_local;
482
- }
483
-
484
- appTz = rb_hash_aref(opts, sym_application_timezone);
485
- if (appTz == sym_local) {
486
- app_timezone = intern_local;
487
- } else if (appTz == sym_utc) {
488
- app_timezone = intern_utc;
489
- } else {
490
- app_timezone = Qnil;
491
- }
492
-
493
773
  if (wrapper->is_streaming) {
494
774
  /* When streaming, we will only yield rows, not return them. */
495
775
  if (wrapper->rows == Qnil) {
@@ -502,10 +782,10 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
502
782
  fields = mysql_fetch_fields(wrapper->result);
503
783
 
504
784
  do {
505
- row = rb_mysql_result_fetch_row(self, db_timezone, app_timezone, symbolizeKeys, asArray, castBool, cast, fields);
785
+ row = fetch_row_func(self, fields, args);
506
786
  if (row != Qnil) {
507
787
  wrapper->numberOfRows++;
508
- if (block != Qnil) {
788
+ if (args->block_given != Qnil) {
509
789
  rb_yield(row);
510
790
  }
511
791
  }
@@ -524,20 +804,7 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
524
804
  rb_raise(cMysql2Error, "You have already fetched all the rows for this query and streaming is true. (to reiterate you must requery).");
525
805
  }
526
806
  } else {
527
- if (wrapper->lastRowProcessed == 0) {
528
- wrapper->numberOfRows = mysql_num_rows(wrapper->result);
529
- if (wrapper->numberOfRows == 0) {
530
- wrapper->rows = rb_ary_new();
531
- return wrapper->rows;
532
- }
533
- wrapper->rows = rb_ary_new2(wrapper->numberOfRows);
534
- } else if (!cacheRows && wrapper->lastRowProcessed == wrapper->numberOfRows) {
535
- mysql_data_seek(wrapper->result, 0);
536
- wrapper->lastRowProcessed = 0;
537
- wrapper->rows = rb_ary_new2(wrapper->numberOfRows);
538
- }
539
-
540
- if (cacheRows && wrapper->lastRowProcessed == wrapper->numberOfRows) {
807
+ if (args->cacheRows && wrapper->lastRowProcessed == wrapper->numberOfRows) {
541
808
  /* we've already read the entire dataset from the C result into our */
542
809
  /* internal array. Lets hand that over to the user since it's ready to go */
543
810
  for (i = 0; i < wrapper->numberOfRows; i++) {
@@ -550,11 +817,11 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
550
817
 
551
818
  for (i = 0; i < wrapper->numberOfRows; i++) {
552
819
  VALUE row;
553
- if (cacheRows && i < rowsProcessed) {
820
+ if (args->cacheRows && i < rowsProcessed) {
554
821
  row = rb_ary_entry(wrapper->rows, i);
555
822
  } else {
556
- row = rb_mysql_result_fetch_row(self, db_timezone, app_timezone, symbolizeKeys, asArray, castBool, cast, fields);
557
- if (cacheRows) {
823
+ row = fetch_row_func(self, fields, args);
824
+ if (args->cacheRows) {
558
825
  rb_ary_store(wrapper->rows, i, row);
559
826
  }
560
827
  wrapper->lastRowProcessed++;
@@ -562,26 +829,113 @@ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
562
829
 
563
830
  if (row == Qnil) {
564
831
  /* we don't need the mysql C dataset around anymore, peace it */
565
- if (cacheRows) {
566
- rb_mysql_result_free_result(wrapper);
567
- }
832
+ rb_mysql_result_free_result(wrapper);
568
833
  return Qnil;
569
834
  }
570
835
 
571
- if (block != Qnil) {
836
+ if (args->block_given != Qnil) {
572
837
  rb_yield(row);
573
838
  }
574
839
  }
575
- if (wrapper->lastRowProcessed == wrapper->numberOfRows && cacheRows) {
840
+ if (wrapper->lastRowProcessed == wrapper->numberOfRows) {
576
841
  /* we don't need the mysql C dataset around anymore, peace it */
577
842
  rb_mysql_result_free_result(wrapper);
578
843
  }
579
844
  }
580
845
  }
581
846
 
847
+ // FIXME return Enumerator instead?
848
+ // return rb_ary_each(wrapper->rows);
582
849
  return wrapper->rows;
583
850
  }
584
851
 
852
+ static VALUE rb_mysql_result_each(int argc, VALUE * argv, VALUE self) {
853
+ result_each_args args;
854
+ VALUE defaults, opts, block, (*fetch_row_func)(VALUE, MYSQL_FIELD *fields, const result_each_args *args);
855
+ ID db_timezone, app_timezone, dbTz, appTz;
856
+ int symbolizeKeys, asArray, castBool, cacheRows, cast;
857
+
858
+ GET_RESULT(self);
859
+
860
+ if (wrapper->stmt_wrapper && wrapper->stmt_wrapper->closed) {
861
+ rb_raise(cMysql2Error, "Statement handle already closed");
862
+ }
863
+
864
+ defaults = rb_iv_get(self, "@query_options");
865
+ Check_Type(defaults, T_HASH);
866
+ if (rb_scan_args(argc, argv, "01&", &opts, &block) == 1) {
867
+ opts = rb_funcall(defaults, intern_merge, 1, opts);
868
+ } else {
869
+ opts = defaults;
870
+ }
871
+
872
+ symbolizeKeys = RTEST(rb_hash_aref(opts, sym_symbolize_keys));
873
+ asArray = rb_hash_aref(opts, sym_as) == sym_array;
874
+ castBool = RTEST(rb_hash_aref(opts, sym_cast_booleans));
875
+ cacheRows = RTEST(rb_hash_aref(opts, sym_cache_rows));
876
+ cast = RTEST(rb_hash_aref(opts, sym_cast));
877
+
878
+ if (wrapper->is_streaming && cacheRows) {
879
+ rb_warn(":cache_rows is ignored if :stream is true");
880
+ }
881
+
882
+ if (wrapper->stmt_wrapper && !cacheRows && !wrapper->is_streaming) {
883
+ rb_warn(":cache_rows is forced for prepared statements (if not streaming)");
884
+ }
885
+
886
+ if (wrapper->stmt_wrapper && !cast) {
887
+ rb_warn(":cast is forced for prepared statements");
888
+ }
889
+
890
+ dbTz = rb_hash_aref(opts, sym_database_timezone);
891
+ if (dbTz == sym_local) {
892
+ db_timezone = intern_local;
893
+ } else if (dbTz == sym_utc) {
894
+ db_timezone = intern_utc;
895
+ } else {
896
+ if (!NIL_P(dbTz)) {
897
+ rb_warn(":database_timezone option must be :utc or :local - defaulting to :local");
898
+ }
899
+ db_timezone = intern_local;
900
+ }
901
+
902
+ appTz = rb_hash_aref(opts, sym_application_timezone);
903
+ if (appTz == sym_local) {
904
+ app_timezone = intern_local;
905
+ } else if (appTz == sym_utc) {
906
+ app_timezone = intern_utc;
907
+ } else {
908
+ app_timezone = Qnil;
909
+ }
910
+
911
+ if (wrapper->lastRowProcessed == 0 && !wrapper->is_streaming) {
912
+ wrapper->numberOfRows = wrapper->stmt_wrapper ? mysql_stmt_num_rows(wrapper->stmt_wrapper->stmt) : mysql_num_rows(wrapper->result);
913
+ if (wrapper->numberOfRows == 0) {
914
+ wrapper->rows = rb_ary_new();
915
+ return wrapper->rows;
916
+ }
917
+ wrapper->rows = rb_ary_new2(wrapper->numberOfRows);
918
+ }
919
+
920
+ // Backward compat
921
+ args.symbolizeKeys = symbolizeKeys;
922
+ args.asArray = asArray;
923
+ args.castBool = castBool;
924
+ args.cacheRows = cacheRows;
925
+ args.cast = cast;
926
+ args.db_timezone = db_timezone;
927
+ args.app_timezone = app_timezone;
928
+ args.block_given = block;
929
+
930
+ if (wrapper->stmt_wrapper) {
931
+ fetch_row_func = rb_mysql_result_fetch_row_stmt;
932
+ } else {
933
+ fetch_row_func = rb_mysql_result_fetch_row;
934
+ }
935
+
936
+ return rb_mysql_result_each_(self, fetch_row_func, &args);
937
+ }
938
+
585
939
  static VALUE rb_mysql_result_count(VALUE self) {
586
940
  GET_RESULT(self);
587
941
 
@@ -595,12 +949,16 @@ static VALUE rb_mysql_result_count(VALUE self) {
595
949
  return LONG2NUM(RARRAY_LEN(wrapper->rows));
596
950
  } else {
597
951
  /* MySQL returns an unsigned 64-bit long here */
598
- return ULL2NUM(mysql_num_rows(wrapper->result));
952
+ if (wrapper->stmt_wrapper) {
953
+ return ULL2NUM(mysql_stmt_num_rows(wrapper->stmt_wrapper->stmt));
954
+ } else {
955
+ return ULL2NUM(mysql_num_rows(wrapper->result));
956
+ }
599
957
  }
600
958
  }
601
959
 
602
960
  /* Mysql2::Result */
603
- VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_RES *r) {
961
+ VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_RES *r, VALUE statement) {
604
962
  VALUE obj;
605
963
  mysql2_result_wrapper * wrapper;
606
964
 
@@ -617,9 +975,21 @@ VALUE rb_mysql_result_to_obj(VALUE client, VALUE encoding, VALUE options, MYSQL_
617
975
  wrapper->client = client;
618
976
  wrapper->client_wrapper = DATA_PTR(client);
619
977
  wrapper->client_wrapper->refcount++;
978
+ wrapper->result_buffers = NULL;
979
+ wrapper->is_null = NULL;
980
+ wrapper->error = NULL;
981
+ wrapper->length = NULL;
982
+
983
+ /* Keep a handle to the Statement to ensure it doesn't get garbage collected first */
984
+ wrapper->statement = statement;
985
+ if (statement != Qnil) {
986
+ wrapper->stmt_wrapper = DATA_PTR(statement);
987
+ wrapper->stmt_wrapper->refcount++;
988
+ } else {
989
+ wrapper->stmt_wrapper = NULL;
990
+ }
620
991
 
621
992
  rb_obj_call_init(obj, 0, NULL);
622
-
623
993
  rb_iv_set(obj, "@query_options", options);
624
994
 
625
995
  /* Options that cannot be changed in results.each(...) { |row| }