do_mysql 0.2.4 → 0.9.2

Sign up to get free protection for your applications and to get access to all the features.
data/Rakefile CHANGED
@@ -1,36 +1,59 @@
1
1
  require 'rubygems'
2
+ require 'rake/clean'
2
3
  require 'rake/gempackagetask'
4
+ require 'spec/rake/spectask'
5
+ require 'pathname'
6
+ require Pathname(__FILE__).dirname.expand_path.parent + 'tasks/ext_helper'
7
+ require Pathname(__FILE__).dirname.expand_path.parent + 'tasks/ext_helper_java'
3
8
 
4
- PLUGIN = "do_mysql"
5
- NAME = "do_mysql"
6
- VERSION = "0.2.4"
7
- AUTHOR = "Yehuda Katz"
8
- EMAIL = "wycats@gmail.com"
9
- HOMEPAGE = "http://dataobjects.devjavu.com"
10
- SUMMARY = "A DataObject.rb driver for mysql"
9
+ # House-keeping
10
+ CLEAN.include '**/*.o', '**/*.so', '**/*.bundle', '**/*.a',
11
+ '**/*.log', '{ext,lib}/*.{bundle,so,obj,pdb,lib,def,exp}',
12
+ 'ext/Makefile'
13
+
14
+ JRUBY = (RUBY_PLATFORM =~ /java/) rescue nil
15
+ WINDOWS = (RUBY_PLATFORM =~ /mswin|mingw|cygwin/) rescue nil
16
+ # don't use SUDO with JRuby, for the moment, although this behaviour
17
+ # is not entirely correct.
18
+ SUDO = (WINDOWS || JRUBY) ? '' : ('sudo' unless ENV['SUDOLESS'])
11
19
 
12
20
  spec = Gem::Specification.new do |s|
13
- s.name = NAME
14
- s.version = VERSION
15
- s.platform = Gem::Platform::RUBY
16
- s.has_rdoc = true
17
- s.extra_rdoc_files = ["README", "LICENSE", 'TODO']
18
- s.summary = SUMMARY
19
- s.description = s.summary
20
- s.author = AUTHOR
21
- s.email = EMAIL
22
- s.homepage = HOMEPAGE
23
- s.add_dependency('data_objects', ["<=0.2.0"])
24
- s.require_path = 'lib'
25
- s.autorequire = PLUGIN
26
- s.extensions = ["ext/extconf.rb"]
27
- s.files = %w(LICENSE README Rakefile TODO) + Dir.glob("{lib,specs,ext}/**/*").reject {|x| x =~ /\.(o|bundle)$/ }
21
+ s.name = 'do_mysql'
22
+ s.version = '0.9.2'
23
+ s.platform = Gem::Platform::RUBY
24
+ s.has_rdoc = false
25
+ s.extra_rdoc_files = %w[ LICENSE TODO ]
26
+ s.summary = 'A DataObject.rb driver for MySQL'
27
+ s.description = s.summary
28
+ s.author = 'Scott Bauer'
29
+ s.email = 'bauer.mail@gmail.com'
30
+ s.homepage = 'http://rubyforge.org/projects/dorb'
31
+ s.rubyforge_project = 'dorb'
32
+ s.require_path = 'lib'
33
+ s.extensions = %w[ ext/extconf.rb ]
34
+ s.files = FileList[ '{ext,lib,spec}/**/*.{c,rb}', 'Rakefile', *s.extra_rdoc_files ]
35
+ s.add_dependency('data_objects', "=#{s.version}")
28
36
  end
29
37
 
30
38
  Rake::GemPackageTask.new(spec) do |pkg|
31
39
  pkg.gem_spec = spec
32
40
  end
33
41
 
34
- task :install => [:package] do
35
- sh %{sudo gem install pkg/#{NAME}-#{VERSION}}
36
- end
42
+ # Use of ext_helper to properly setup compile tasks and native gem generation
43
+ setup_extension "#{spec.name}_ext", spec
44
+ setup_extension_java "#{spec.name}_ext", spec
45
+
46
+ task :install => [ :package ] do
47
+ sh %{#{SUDO} gem install --local pkg/#{spec.name}-#{spec.version} --no-update-sources}, :verbose => false
48
+ end
49
+
50
+ desc "Uninstall #{spec.name} #{spec.version} (default ruby)"
51
+ task :uninstall => [ :clobber ] do
52
+ sh "#{SUDO} gem uninstall #{spec.name} -v#{spec.version} -I -x", :verbose => false
53
+ end
54
+
55
+ desc 'Run specifications'
56
+ Spec::Rake::SpecTask.new(:spec => [ :compile ]) do |t|
57
+ t.spec_opts << '--options' << 'spec/spec.opts' if File.exists?('spec/spec.opts')
58
+ t.spec_files = Pathname.glob(Pathname.new(__FILE__).dirname + 'spec/**/*_spec.rb')
59
+ end
data/TODO CHANGED
@@ -0,0 +1,4 @@
1
+ TODO
2
+ ====
3
+
4
+ * Add JDBC-based version of this driver.
@@ -0,0 +1,807 @@
1
+ #include <ruby.h>
2
+ #include <version.h>
3
+ #include <string.h>
4
+ #include <math.h>
5
+ #include <ctype.h>
6
+ #include <time.h>
7
+ #include <mysql.h>
8
+ #include <errmsg.h>
9
+ #include <mysqld_error.h>
10
+
11
+ #define RUBY_CLASS(name) rb_const_get(rb_cObject, rb_intern(name))
12
+ #define RUBY_STRING(char_ptr) rb_str_new2(char_ptr)
13
+ #define TAINTED_STRING(name) rb_tainted_str_new2(name)
14
+ #define DRIVER_CLASS(klass, parent) (rb_define_class_under(mDOMysql, klass, parent))
15
+ #define CONST_GET(scope, constant) (rb_funcall(scope, ID_CONST_GET, 1, rb_str_new2(constant)))
16
+ #define CHECK_AND_RAISE(mysql_result_value) if (0 != mysql_result_value) { raise_mysql_error(db, mysql_result_value); }
17
+ #define PUTS(string) rb_funcall(rb_mKernel, rb_intern("puts"), 1, RUBY_STRING(string))
18
+
19
+ #ifdef _WIN32
20
+ #define do_int64 signed __int64
21
+ #else
22
+ #define do_int64 signed long long int
23
+ #endif
24
+
25
+ // To store rb_intern values
26
+ static ID ID_TO_I;
27
+ static ID ID_TO_F;
28
+ static ID ID_TO_S;
29
+ static ID ID_PARSE;
30
+ static ID ID_TO_TIME;
31
+ static ID ID_NEW;
32
+ static ID ID_NEW_RATIONAL;
33
+ static ID ID_NEW_DATE;
34
+ static ID ID_CONST_GET;
35
+ static ID ID_UTC;
36
+ static ID ID_ESCAPE_SQL;
37
+ static ID ID_STRFTIME;
38
+ static ID ID_LOGGER;
39
+ static ID ID_DEBUG;
40
+ static ID ID_LEVEL;
41
+
42
+ // References to DataObjects base classes
43
+ static VALUE mDO;
44
+ static VALUE cDO_Quoting;
45
+ static VALUE cDO_Connection;
46
+ static VALUE cDO_Command;
47
+ static VALUE cDO_Result;
48
+ static VALUE cDO_Reader;
49
+
50
+ // References to Ruby classes that we'll need
51
+ static VALUE rb_cDate;
52
+ static VALUE rb_cDateTime;
53
+ static VALUE rb_cRational;
54
+ static VALUE rb_cBigDecimal;
55
+ static VALUE rb_cCGI;
56
+
57
+ // Classes that we'll build in Init
58
+ static VALUE mDOMysql;
59
+ static VALUE cConnection;
60
+ static VALUE cCommand;
61
+ static VALUE cResult;
62
+ static VALUE cReader;
63
+ static VALUE eMysqlError;
64
+
65
+ // Figures out what we should cast a given mysql field type to
66
+ static char * ruby_type_from_mysql_type(MYSQL_FIELD *field) {
67
+
68
+ char* ruby_type_name;
69
+
70
+ switch(field->type) {
71
+ case MYSQL_TYPE_NULL: {
72
+ ruby_type_name = NULL;
73
+ break;
74
+ }
75
+ case MYSQL_TYPE_TINY: {
76
+ ruby_type_name = "TrueClass";
77
+ break;
78
+ }
79
+ case MYSQL_TYPE_SHORT:
80
+ case MYSQL_TYPE_LONG:
81
+ case MYSQL_TYPE_INT24:
82
+ case MYSQL_TYPE_LONGLONG:
83
+ case MYSQL_TYPE_YEAR: {
84
+ ruby_type_name = "Fixnum";
85
+ break;
86
+ }
87
+ case MYSQL_TYPE_DECIMAL:
88
+ case MYSQL_TYPE_FLOAT:
89
+ case MYSQL_TYPE_DOUBLE: {
90
+ ruby_type_name = "BigDecimal";
91
+ break;
92
+ }
93
+ case MYSQL_TYPE_TIMESTAMP:
94
+ case MYSQL_TYPE_DATETIME: {
95
+ ruby_type_name = "DateTime";
96
+ break;
97
+ }
98
+ case MYSQL_TYPE_TIME: {
99
+ ruby_type_name = "DateTime";
100
+ break;
101
+ }
102
+ case MYSQL_TYPE_DATE: {
103
+ ruby_type_name = "Date";
104
+ break;
105
+ }
106
+ default: {
107
+ // printf("Falling to default: %s - %d\n", field->name, field->type);
108
+ ruby_type_name = "String";
109
+ }
110
+ }
111
+
112
+ return ruby_type_name;
113
+ }
114
+
115
+ // Find the greatest common denominator and reduce the provided numerator and denominator.
116
+ // This replaces calles to Rational.reduce! which does the same thing, but really slowly.
117
+ static void reduce( do_int64 *numerator, do_int64 *denominator ) {
118
+ do_int64 a, b, c;
119
+ a = *numerator;
120
+ b = *denominator;
121
+ while ( a != 0 ) {
122
+ c = a; a = b % a; b = c;
123
+ }
124
+ *numerator = *numerator / b;
125
+ *denominator = *denominator / b;
126
+ }
127
+
128
+ // Generate the date integer which Date.civil_to_jd returns
129
+ static int jd_from_date(int year, int month, int day) {
130
+ int a, b;
131
+ if ( month <= 2 ) {
132
+ year -= 1;
133
+ month += 12;
134
+ }
135
+ a = year / 100;
136
+ b = 2 - a + (a / 4);
137
+ return floor(365.25 * (year + 4716)) + floor(30.6001 * (month + 1)) + day + b - 1524;
138
+ }
139
+
140
+ static VALUE seconds_to_offset(long seconds_offset) {
141
+ do_int64 num = seconds_offset, den = 86400;
142
+ reduce(&num, &den);
143
+ return rb_funcall(rb_cRational, rb_intern("new!"), 2, rb_ll2inum(num), rb_ll2inum(den));
144
+ }
145
+
146
+ static VALUE parse_date(const char *date) {
147
+ int year, month, day;
148
+ int jd, ajd;
149
+ VALUE rational;
150
+
151
+ sscanf(date, "%4d-%2d-%2d", &year, &month, &day);
152
+
153
+ jd = jd_from_date(year, month, day);
154
+
155
+ // Math from Date.jd_to_ajd
156
+ ajd = jd * 2 - 1;
157
+ rational = rb_funcall(rb_cRational, ID_NEW_RATIONAL, 2, INT2NUM(ajd), INT2NUM(2));
158
+ return rb_funcall(rb_cDate, ID_NEW_DATE, 3, rational, INT2NUM(0), INT2NUM(2299161));
159
+ }
160
+
161
+ static VALUE parse_time(const char *date) {
162
+
163
+ int year, month, day, hour, min, sec, usec;
164
+ char subsec[7];
165
+
166
+ if (0 != strchr(date, '.')) {
167
+ // right padding usec with 0. e.g. '012' will become 12000 microsecond, since Time#local use microsecond
168
+ sscanf(date, "%4d-%2d-%2d %2d:%2d:%2d.%s", &year, &month, &day, &hour, &min, &sec, subsec);
169
+ sscanf(subsec, "%d", &usec);
170
+ } else {
171
+ sscanf(date, "%4d-%2d-%2d %2d:%2d:%2d", &year, &month, &day, &hour, &min, &sec);
172
+ usec = 0;
173
+ }
174
+
175
+ return rb_funcall(rb_cTime, rb_intern("local"), 7, INT2NUM(year), INT2NUM(month), INT2NUM(day), INT2NUM(hour), INT2NUM(min), INT2NUM(sec), INT2NUM(usec));
176
+ }
177
+
178
+ static VALUE parse_date_time(const char *date_time) {
179
+ VALUE ajd, offset;
180
+
181
+ int year, month, day, hour, min, sec;
182
+ int jd;
183
+ do_int64 num, den;
184
+
185
+ time_t rawtime;
186
+ struct tm * timeinfo;
187
+
188
+ // Mysql date format: 2008-05-03 14:43:00
189
+ sscanf(date_time, "%4d-%2d-%2d %2d:%2d:%2d", &year, &month, &day, &hour, &min, &sec);
190
+
191
+ jd = jd_from_date(year, month, day);
192
+
193
+ // Generate ajd with fractional days for the time
194
+ // Extracted from Date#jd_to_ajd, Date#day_fraction_to_time, and Rational#+ and #-
195
+ num = ((hour) * 1440) + ((min) * 24); // (Hour * Minutes in a day) + (minutes * 24)
196
+
197
+ // Get localtime
198
+ time(&rawtime);
199
+ timeinfo = localtime(&rawtime);
200
+
201
+ // TODO: Refactor the following few lines to do the calculation with the *seconds*
202
+ // value instead of having to do the hour/minute math
203
+ int hour_offset = abs(timeinfo->tm_gmtoff) / 3600;
204
+ int minute_offset = abs(timeinfo->tm_gmtoff) % 3600 / 60;
205
+
206
+ // Modify the numerator so when we apply the timezone everything works out
207
+ if (timeinfo->tm_gmtoff < 0) {
208
+ // If the Timezone is behind UTC, we need to add the time offset
209
+ num += (hour_offset * 1440) + (minute_offset * 24);
210
+ } else {
211
+ // If the Timezone is ahead of UTC, we need to subtract the time offset
212
+ num -= (hour_offset * 1440) + (minute_offset * 24);
213
+ }
214
+
215
+ den = (24 * 1440);
216
+ reduce(&num, &den);
217
+
218
+ num = (num * 86400) + (sec * den);
219
+ den = den * 86400;
220
+ reduce(&num, &den);
221
+
222
+ num = (jd * den) + num;
223
+
224
+ num = num * 2 - den;
225
+ den = den * 2;
226
+ reduce(&num, &den);
227
+
228
+ ajd = rb_funcall(rb_cRational, rb_intern("new!"), 2, rb_ull2inum(num), rb_ull2inum(den));
229
+
230
+ // Calculate the offset using the seconds from GMT
231
+ offset = seconds_to_offset(timeinfo->tm_gmtoff);
232
+
233
+ return rb_funcall(rb_cDateTime, ID_NEW_DATE, 3, ajd, offset, INT2NUM(2299161));
234
+ }
235
+
236
+ // Convert C-string to a Ruby instance of Ruby type "type"
237
+ static VALUE typecast(const char* value, char* type) {
238
+ if (NULL == value)
239
+ return Qnil;
240
+
241
+ if ( strcmp(type, "Class") == 0) {
242
+ return rb_funcall(mDO, rb_intern("find_const"), 1, TAINTED_STRING(value));
243
+ } else if ( strcmp(type, "Integer") == 0 || strcmp(type, "Fixnum") == 0 || strcmp(type, "Bignum") == 0 ) {
244
+ return rb_cstr2inum(value, 10);
245
+ } else if (0 == strcmp("String", type)) {
246
+ return TAINTED_STRING(value);
247
+ } else if (0 == strcmp("Float", type) ) {
248
+ return rb_float_new(rb_cstr_to_dbl(value, Qfalse));
249
+ } else if (0 == strcmp("BigDecimal", type) ) {
250
+ return rb_funcall(rb_cBigDecimal, ID_NEW, 1, TAINTED_STRING(value));
251
+ } else if (0 == strcmp("TrueClass", type) || 0 == strcmp("FalseClass", type)) {
252
+ return (0 == value || 0 == strcmp("0", value)) ? Qfalse : Qtrue;
253
+ } else if (0 == strcmp("Date", type)) {
254
+ return parse_date(value);
255
+ } else if (0 == strcmp("DateTime", type)) {
256
+ return parse_date_time(value);
257
+ } else if (0 == strcmp("Time", type)) {
258
+ return parse_time(value);
259
+ } else {
260
+ return TAINTED_STRING(value);
261
+ }
262
+ }
263
+
264
+ static void data_objects_debug(VALUE string) {
265
+ VALUE logger = rb_funcall(mDOMysql, ID_LOGGER, 0);
266
+ int log_level = NUM2INT(rb_funcall(logger, ID_LEVEL, 0));
267
+
268
+ if (0 == log_level) {
269
+ rb_funcall(logger, ID_DEBUG, 1, string);
270
+ }
271
+ }
272
+
273
+ // We can add custom information to error messages using this function
274
+ // if we think it matters
275
+ static void raise_mysql_error(MYSQL *db, int mysql_error_code) {
276
+ char *error_message = (char *)mysql_error(db);
277
+
278
+ switch(mysql_error_code) {
279
+ case CR_UNKNOWN_ERROR:
280
+ case CR_SOCKET_CREATE_ERROR:
281
+ case CR_CONNECTION_ERROR:
282
+ case CR_CONN_HOST_ERROR:
283
+ case CR_IPSOCK_ERROR:
284
+ case CR_UNKNOWN_HOST:
285
+ case CR_SERVER_GONE_ERROR:
286
+ case CR_VERSION_ERROR:
287
+ case CR_OUT_OF_MEMORY:
288
+ case CR_WRONG_HOST_INFO:
289
+ case CR_LOCALHOST_CONNECTION:
290
+ case CR_TCP_CONNECTION:
291
+ case CR_SERVER_HANDSHAKE_ERR:
292
+ case CR_SERVER_LOST:
293
+ case CR_COMMANDS_OUT_OF_SYNC:
294
+ case CR_NAMEDPIPE_CONNECTION:
295
+ case CR_NAMEDPIPEWAIT_ERROR:
296
+ case CR_NAMEDPIPEOPEN_ERROR:
297
+ case CR_NAMEDPIPESETSTATE_ERROR:
298
+ case CR_CANT_READ_CHARSET:
299
+ case CR_NET_PACKET_TOO_LARGE:
300
+ case CR_EMBEDDED_CONNECTION:
301
+ case CR_PROBE_SLAVE_STATUS:
302
+ case CR_PROBE_SLAVE_HOSTS:
303
+ case CR_PROBE_SLAVE_CONNECT:
304
+ case CR_PROBE_MASTER_CONNECT:
305
+ case CR_SSL_CONNECTION_ERROR:
306
+ case CR_MALFORMED_PACKET:
307
+ case CR_WRONG_LICENSE:
308
+ case CR_NULL_POINTER:
309
+ case CR_NO_PREPARE_STMT:
310
+ case CR_PARAMS_NOT_BOUND:
311
+ case CR_DATA_TRUNCATED:
312
+ case CR_NO_PARAMETERS_EXISTS:
313
+ case CR_INVALID_PARAMETER_NO:
314
+ case CR_INVALID_BUFFER_USE:
315
+ case CR_UNSUPPORTED_PARAM_TYPE:
316
+ case CR_SHARED_MEMORY_CONNECTION:
317
+ case CR_SHARED_MEMORY_CONNECT_REQUEST_ERROR:
318
+ case CR_SHARED_MEMORY_CONNECT_ANSWER_ERROR:
319
+ case CR_SHARED_MEMORY_CONNECT_FILE_MAP_ERROR:
320
+ case CR_SHARED_MEMORY_CONNECT_MAP_ERROR:
321
+ case CR_SHARED_MEMORY_FILE_MAP_ERROR:
322
+ case CR_SHARED_MEMORY_MAP_ERROR:
323
+ case CR_SHARED_MEMORY_EVENT_ERROR:
324
+ case CR_SHARED_MEMORY_CONNECT_ABANDONED_ERROR:
325
+ case CR_SHARED_MEMORY_CONNECT_SET_ERROR:
326
+ case CR_CONN_UNKNOW_PROTOCOL:
327
+ case CR_INVALID_CONN_HANDLE:
328
+ case CR_SECURE_AUTH:
329
+ case CR_FETCH_CANCELED:
330
+ case CR_NO_DATA:
331
+ case CR_NO_STMT_METADATA:
332
+ #if MYSQL_VERSION_ID >= 50000
333
+ case CR_NO_RESULT_SET:
334
+ case CR_NOT_IMPLEMENTED:
335
+ #endif
336
+ {
337
+ break;
338
+ }
339
+ default: {
340
+ // Hmmm
341
+ break;
342
+ }
343
+ }
344
+
345
+ rb_raise(eMysqlError, error_message);
346
+ }
347
+
348
+ // Pull an option out of a querystring-formmated option list using CGI::parse
349
+ static char * get_uri_option(VALUE querystring, char * key) {
350
+ VALUE options_hash, option_value;
351
+
352
+ char * value = NULL;
353
+
354
+ // Ensure that we're dealing with a string
355
+ querystring = rb_funcall(querystring, ID_TO_S, 0);
356
+
357
+ options_hash = rb_funcall(rb_cCGI, ID_PARSE, 1, querystring);
358
+
359
+ // TODO: rb_hash_aref always returns an array?
360
+ option_value = rb_ary_entry(rb_hash_aref(options_hash, RUBY_STRING(key)), 0);
361
+
362
+ if (Qnil != option_value) {
363
+ value = StringValuePtr(option_value);
364
+ }
365
+
366
+ return value;
367
+ }
368
+
369
+ static VALUE cConnection_initialize(VALUE self, VALUE uri) {
370
+ VALUE r_host, r_user, r_password, r_path, r_options, r_port;
371
+
372
+ char *host = "localhost", *user = "root", *password = NULL, *path;
373
+ char *database = "", *socket = NULL;
374
+ char *charset = NULL;
375
+
376
+ int port = 3306;
377
+ unsigned long client_flags = 0;
378
+ int charset_error;
379
+
380
+ MYSQL *db = 0, *result;
381
+ db = (MYSQL *)mysql_init(NULL);
382
+
383
+ rb_iv_set(self, "@using_socket", Qfalse);
384
+
385
+ r_host = rb_funcall(uri, rb_intern("host"), 0);
386
+ if (Qnil != r_host) {
387
+ host = StringValuePtr(r_host);
388
+ }
389
+
390
+ r_user = rb_funcall(uri, rb_intern("user"), 0);
391
+ if (Qnil != r_user) {
392
+ user = StringValuePtr(r_user);
393
+ }
394
+
395
+ r_password = rb_funcall(uri, rb_intern("password"), 0);
396
+ if (Qnil != r_password) {
397
+ password = StringValuePtr(r_password);
398
+ }
399
+
400
+ r_path = rb_funcall(uri, rb_intern("path"), 0);
401
+ path = StringValuePtr(r_path);
402
+ if (Qnil != r_path) {
403
+ database = strtok(path, "/");
404
+ }
405
+
406
+ if (NULL == database || 0 == strlen(database)) {
407
+ rb_raise(eMysqlError, "Database must be specified");
408
+ }
409
+
410
+ // Pull the querystring off the URI
411
+ r_options = rb_funcall(uri, rb_intern("query"), 0);
412
+
413
+ // Check to see if we're on the db machine. If so, try to use the socket
414
+ if (0 == strcasecmp(host, "localhost")) {
415
+ socket = get_uri_option(r_options, "socket");
416
+ if (NULL != socket) {
417
+ rb_iv_set(self, "@using_socket", Qtrue);
418
+ }
419
+ }
420
+
421
+ r_port = rb_funcall(uri, rb_intern("port"), 0);
422
+ if (Qnil != r_port) {
423
+ port = NUM2INT(r_port);
424
+ }
425
+
426
+ charset = get_uri_option(r_options, "charset");
427
+
428
+ // If ssl? {
429
+ // mysql_ssl_set(db, key, cert, ca, capath, cipher)
430
+ // }
431
+
432
+ result = (MYSQL *)mysql_real_connect(
433
+ db,
434
+ host,
435
+ user,
436
+ password,
437
+ database,
438
+ port,
439
+ socket,
440
+ client_flags
441
+ );
442
+
443
+ if (NULL == result) {
444
+ raise_mysql_error(db, -1);
445
+ }
446
+
447
+ if (NULL == charset) {
448
+ charset = (char*)calloc(5, sizeof(char));
449
+ strcpy(charset, "utf8");
450
+ }
451
+
452
+ // Set the connections character set
453
+ charset_error = mysql_set_character_set(db, charset);
454
+ if (0 != charset_error) {
455
+ raise_mysql_error(db, charset_error);
456
+ }
457
+
458
+ rb_iv_set(self, "@uri", uri);
459
+ rb_iv_set(self, "@connection", Data_Wrap_Struct(rb_cObject, 0, 0, db));
460
+
461
+ return Qtrue;
462
+ }
463
+
464
+ static VALUE cConnection_character_set(VALUE self) {
465
+ VALUE connection_container = rb_iv_get(self, "@connection");
466
+ MYSQL *db;
467
+
468
+ const char *charset;
469
+
470
+ if (Qnil == connection_container)
471
+ return Qfalse;
472
+
473
+ db = DATA_PTR(connection_container);
474
+
475
+ charset = mysql_character_set_name(db);
476
+
477
+ return RUBY_STRING(charset);
478
+ }
479
+
480
+ static VALUE cConnection_is_using_socket(VALUE self) {
481
+ return rb_iv_get(self, "@using_socket");
482
+ }
483
+
484
+ static VALUE cConnection_dispose(VALUE self) {
485
+ VALUE connection_container = rb_iv_get(self, "@connection");
486
+
487
+ MYSQL *db;
488
+
489
+ if (Qnil == connection_container)
490
+ return Qfalse;
491
+
492
+ db = DATA_PTR(connection_container);
493
+
494
+ if (NULL == db)
495
+ return Qfalse;
496
+
497
+ mysql_close(db);
498
+ rb_iv_set(self, "@connection", Qnil);
499
+
500
+ return Qtrue;
501
+ }
502
+
503
+ /*
504
+ Accepts an array of Ruby types (Fixnum, Float, String, etc...) and turns them
505
+ into Ruby-strings so we can easily typecast later
506
+ */
507
+ static VALUE cCommand_set_types(VALUE self, VALUE array) {
508
+ VALUE type_strings = rb_ary_new();
509
+ int i;
510
+
511
+ for (i = 0; i < RARRAY(array)->len; i++) {
512
+ rb_ary_push(type_strings, RUBY_STRING(rb_class2name(rb_ary_entry(array, i))));
513
+ }
514
+
515
+ rb_iv_set(self, "@field_types", type_strings);
516
+
517
+ return array;
518
+ }
519
+
520
+ VALUE cCommand_quote_time(VALUE self, VALUE value) {
521
+ return rb_funcall(value, ID_STRFTIME, 1, RUBY_STRING("'%Y-%m-%d %H:%M:%S'"));
522
+ }
523
+
524
+
525
+ VALUE cCommand_quote_date_time(VALUE self, VALUE value) {
526
+ // TODO: Support non-local dates. we need to call #new_offset on the date to be
527
+ // quoted and pass in the current locale's date offset (self.new_offset((hours * 3600).to_r / 86400)
528
+ return rb_funcall(value, ID_STRFTIME, 1, RUBY_STRING("'%Y-%m-%d %H:%M:%S'"));
529
+ }
530
+
531
+ VALUE cCommand_quote_date(VALUE self, VALUE value) {
532
+ return rb_funcall(value, ID_STRFTIME, 1, RUBY_STRING("'%Y-%m-%d'"));
533
+ }
534
+
535
+ static VALUE cCommand_quote_string(VALUE self, VALUE string) {
536
+ MYSQL *db = DATA_PTR(rb_iv_get(rb_iv_get(self, "@connection"), "@connection"));
537
+ const char *source = StringValuePtr(string);
538
+ char *escaped;
539
+ VALUE result;
540
+
541
+ int quoted_length = 0;
542
+
543
+ // Allocate space for the escaped version of 'string'. Use + 3 allocate space for null term.
544
+ // and the leading and trailing single-quotes.
545
+ // Thanks to http://www.browardphp.com/mysql_manual_en/manual_MySQL_APIs.html#mysql_real_escape_string
546
+ escaped = (char *)calloc(strlen(source) * 3 + 3, sizeof(char));
547
+
548
+ // Escape 'source' using the current charset in use on the conection 'db'
549
+ quoted_length = mysql_real_escape_string(db, escaped + 1, source, strlen(source));
550
+
551
+ // Wrap the escaped string in single-quotes, this is DO's convention
552
+ escaped[0] = escaped[quoted_length + 1] = '\'';
553
+ result = rb_str_new(escaped, quoted_length + 2);
554
+ free(escaped);
555
+ return result;
556
+ }
557
+
558
+ static VALUE build_query_from_args(VALUE klass, int count, VALUE *args) {
559
+ VALUE query = rb_iv_get(klass, "@text");
560
+ if ( count > 0 ) {
561
+ int i;
562
+ VALUE array = rb_ary_new();
563
+ for ( i = 0; i < count; i++) {
564
+ rb_ary_push(array, (VALUE)args[i]);
565
+ }
566
+ query = rb_funcall(klass, ID_ESCAPE_SQL, 1, array);
567
+ }
568
+ return query;
569
+ }
570
+
571
+ static VALUE cCommand_execute_non_query(int argc, VALUE *argv, VALUE self) {
572
+ VALUE query;
573
+
574
+ MYSQL_RES *response = 0;
575
+ int query_result = 0;
576
+
577
+ my_ulonglong affected_rows;
578
+ MYSQL *db = DATA_PTR(rb_iv_get(rb_iv_get(self, "@connection"), "@connection"));
579
+ query = build_query_from_args(self, argc, argv);
580
+
581
+ data_objects_debug(query);
582
+
583
+ query_result = mysql_query(db, StringValuePtr(query));
584
+ CHECK_AND_RAISE(query_result);
585
+
586
+ response = (MYSQL_RES *)mysql_store_result(db);
587
+ affected_rows = mysql_affected_rows(db);
588
+ mysql_free_result(response);
589
+
590
+ if (-1 == affected_rows)
591
+ return Qnil;
592
+
593
+ return rb_funcall(cResult, ID_NEW, 3, self, INT2NUM(affected_rows), INT2NUM(mysql_insert_id(db)));
594
+ }
595
+
596
+ static VALUE cCommand_execute_reader(int argc, VALUE *argv, VALUE self) {
597
+ VALUE query, reader;
598
+ VALUE field_names, field_types;
599
+
600
+ int query_result = 0;
601
+ int field_count;
602
+ int i;
603
+
604
+ char guess_default_field_types = 0;
605
+
606
+ MYSQL *db = DATA_PTR(rb_iv_get(rb_iv_get(self, "@connection"), "@connection"));
607
+
608
+ MYSQL_RES *response = 0;
609
+ MYSQL_FIELD *field;
610
+
611
+ query = build_query_from_args(self, argc, argv);
612
+ data_objects_debug(query);
613
+
614
+ query_result = mysql_query(db, StringValuePtr(query));
615
+ CHECK_AND_RAISE(query_result);
616
+
617
+ response = (MYSQL_RES *)mysql_use_result(db);
618
+
619
+ if (!response) {
620
+ return Qnil;
621
+ }
622
+
623
+ field_count = (int)mysql_field_count(db);
624
+
625
+ reader = rb_funcall(cReader, ID_NEW, 0);
626
+ rb_iv_set(reader, "@reader", Data_Wrap_Struct(rb_cObject, 0, 0, response));
627
+ rb_iv_set(reader, "@opened", Qtrue);
628
+ rb_iv_set(reader, "@field_count", INT2NUM(field_count));
629
+
630
+ field_names = rb_ary_new();
631
+ field_types = rb_iv_get(self, "@field_types");
632
+
633
+ if ( field_types == Qnil || 0 == RARRAY(field_types)->len ) {
634
+ field_types = rb_ary_new();
635
+ guess_default_field_types = 1;
636
+ } else if (RARRAY(field_types)->len != field_count) {
637
+ // Whoops... wrong number of types passed to set_types. Close the reader and raise
638
+ // and error
639
+ rb_funcall(reader, rb_intern("close"), 0);
640
+ rb_raise(eMysqlError, "Field-count mismatch. Expected %d fields, but the query yielded %d", RARRAY(field_types)->len, field_count);
641
+ }
642
+
643
+ for(i = 0; i < field_count; i++) {
644
+ field = mysql_fetch_field_direct(response, i);
645
+ rb_ary_push(field_names, RUBY_STRING(field->name));
646
+
647
+ if (1 == guess_default_field_types) {
648
+ VALUE field_ruby_type_name = RUBY_STRING(ruby_type_from_mysql_type(field));
649
+ rb_ary_push(field_types, field_ruby_type_name);
650
+ }
651
+ }
652
+
653
+ rb_iv_set(reader, "@fields", field_names);
654
+ rb_iv_set(reader, "@field_types", field_types);
655
+
656
+ if (rb_block_given_p()) {
657
+ rb_yield(reader);
658
+ rb_funcall(reader, rb_intern("close"), 0);
659
+ }
660
+
661
+ return reader;
662
+ }
663
+
664
+ // This should be called to ensure that the internal result reader is freed
665
+ static VALUE cReader_close(VALUE self) {
666
+ // Get the reader from the instance variable, maybe refactor this?
667
+ VALUE reader_container = rb_iv_get(self, "@reader");
668
+
669
+ MYSQL_RES *reader;
670
+
671
+ if (Qnil == reader_container)
672
+ return Qfalse;
673
+
674
+ reader = DATA_PTR(reader_container);
675
+
676
+ // The Meat
677
+ if (NULL == reader)
678
+ return Qfalse;
679
+
680
+ mysql_free_result(reader);
681
+ rb_iv_set(self, "@reader", Qnil);
682
+
683
+ return Qtrue;
684
+ }
685
+
686
+ // Retrieve a single row
687
+ static VALUE cReader_next(VALUE self) {
688
+ // Get the reader from the instance variable, maybe refactor this?
689
+ VALUE reader_container = rb_iv_get(self, "@reader");
690
+ VALUE ruby_field_type_strings, row;
691
+
692
+ MYSQL_RES *reader;
693
+ MYSQL_ROW result;
694
+
695
+ int i;
696
+ char *field_type;
697
+
698
+ if (Qnil == reader_container)
699
+ return Qfalse;
700
+
701
+ reader = DATA_PTR(reader_container);
702
+
703
+ // The Meat
704
+ ruby_field_type_strings = rb_iv_get(self, "@field_types");
705
+ row = rb_ary_new();
706
+ result = (MYSQL_ROW)mysql_fetch_row(reader);
707
+
708
+ rb_iv_set(self, "@state", result ? Qtrue : Qfalse);
709
+
710
+ if (!result)
711
+ return Qnil;
712
+
713
+ for (i = 0; i < reader->field_count; i++) {
714
+ // The field_type data could be cached in a c-array
715
+ field_type = RSTRING(rb_ary_entry(ruby_field_type_strings, i))->ptr;
716
+ rb_ary_push(row, typecast(result[i], field_type));
717
+ }
718
+
719
+ rb_iv_set(self, "@values", row);
720
+
721
+ return Qtrue;
722
+ }
723
+
724
+ static VALUE cReader_values(VALUE self) {
725
+ VALUE state = rb_iv_get(self, "@state");
726
+ if ( state == Qnil || state == Qfalse ) {
727
+ rb_raise(eMysqlError, "Reader is not initialized");
728
+ }
729
+ else {
730
+ return rb_iv_get(self, "@values");
731
+ }
732
+ }
733
+
734
+ static VALUE cReader_fields(VALUE self) {
735
+ return rb_iv_get(self, "@fields");
736
+ }
737
+
738
+ void Init_do_mysql_ext() {
739
+ rb_require("rubygems");
740
+ rb_require("bigdecimal");
741
+ rb_require("date");
742
+ rb_require("cgi");
743
+
744
+ rb_funcall(rb_mKernel, rb_intern("require"), 1, RUBY_STRING("data_objects"));
745
+
746
+ ID_TO_I = rb_intern("to_i");
747
+ ID_TO_F = rb_intern("to_f");
748
+ ID_TO_S = rb_intern("to_s");
749
+ ID_PARSE = rb_intern("parse");
750
+ ID_TO_TIME = rb_intern("to_time");
751
+ ID_NEW = rb_intern("new");
752
+ ID_NEW_RATIONAL = rb_intern("new!");
753
+ ID_NEW_DATE = RUBY_VERSION_CODE < 186 ? rb_intern("new0") : rb_intern("new!");
754
+ ID_CONST_GET = rb_intern("const_get");
755
+ ID_UTC = rb_intern("utc");
756
+ ID_ESCAPE_SQL = rb_intern("escape_sql");
757
+ ID_STRFTIME = rb_intern("strftime");
758
+ ID_LOGGER = rb_intern("logger");
759
+ ID_DEBUG = rb_intern("debug");
760
+ ID_LEVEL = rb_intern("level");
761
+
762
+ // Store references to a few helpful clases that aren't in Ruby Core
763
+ rb_cDate = RUBY_CLASS("Date");
764
+ rb_cDateTime = RUBY_CLASS("DateTime");
765
+ rb_cRational = RUBY_CLASS("Rational");
766
+ rb_cBigDecimal = RUBY_CLASS("BigDecimal");
767
+ rb_cCGI = RUBY_CLASS("CGI");
768
+
769
+ // Get references to the DataObjects module and its classes
770
+ mDO = CONST_GET(rb_mKernel, "DataObjects");
771
+ cDO_Quoting = CONST_GET(mDO, "Quoting");
772
+ cDO_Connection = CONST_GET(mDO, "Connection");
773
+ cDO_Command = CONST_GET(mDO, "Command");
774
+ cDO_Result = CONST_GET(mDO, "Result");
775
+ cDO_Reader = CONST_GET(mDO, "Reader");
776
+
777
+ // Top Level Module that all the classes live under
778
+ mDOMysql = rb_define_module_under(mDO, "Mysql");
779
+
780
+ eMysqlError = rb_define_class("MysqlError", rb_eStandardError);
781
+
782
+ cConnection = DRIVER_CLASS("Connection", cDO_Connection);
783
+ rb_define_method(cConnection, "initialize", cConnection_initialize, 1);
784
+ rb_define_method(cConnection, "using_socket?", cConnection_is_using_socket, 0);
785
+ rb_define_method(cConnection, "character_set", cConnection_character_set , 0);
786
+ rb_define_method(cConnection, "dispose", cConnection_dispose, 0);
787
+
788
+ cCommand = DRIVER_CLASS("Command", cDO_Command);
789
+ rb_include_module(cCommand, cDO_Quoting);
790
+ rb_define_method(cCommand, "set_types", cCommand_set_types, 1);
791
+ rb_define_method(cCommand, "execute_non_query", cCommand_execute_non_query, -1);
792
+ rb_define_method(cCommand, "execute_reader", cCommand_execute_reader, -1);
793
+ rb_define_method(cCommand, "quote_string", cCommand_quote_string, 1);
794
+ rb_define_method(cCommand, "quote_date", cCommand_quote_date, 1);
795
+ rb_define_method(cCommand, "quote_time", cCommand_quote_time, 1);
796
+ rb_define_method(cCommand, "quote_datetime", cCommand_quote_date_time, 1);
797
+
798
+ // Non-Query result
799
+ cResult = DRIVER_CLASS("Result", cDO_Result);
800
+
801
+ // Query result
802
+ cReader = DRIVER_CLASS("Reader", cDO_Reader);
803
+ rb_define_method(cReader, "close", cReader_close, 0);
804
+ rb_define_method(cReader, "next!", cReader_next, 0);
805
+ rb_define_method(cReader, "values", cReader_values, 0);
806
+ rb_define_method(cReader, "fields", cReader_fields, 0);
807
+ }