do_mysql 0.2.4 → 0.9.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.
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
+ }