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 +48 -25
- data/TODO +4 -0
- data/ext/do_mysql_ext.c +807 -0
- data/ext/extconf.rb +51 -50
- data/lib/do_mysql.rb +20 -252
- data/lib/do_mysql/transaction.rb +44 -0
- data/spec/integration/do_mysql_spec.rb +241 -0
- data/spec/integration/logging_spec.rb +51 -0
- data/spec/integration/quoting_spec.rb +37 -0
- data/spec/spec_helper.rb +131 -0
- data/spec/unit/transaction_spec.rb +35 -0
- metadata +60 -50
- data/README +0 -4
- data/ext/mysql_c.c +0 -16602
- data/ext/mysql_c.i +0 -67
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
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
14
|
-
s.version
|
15
|
-
s.platform
|
16
|
-
s.has_rdoc
|
17
|
-
s.extra_rdoc_files
|
18
|
-
s.summary
|
19
|
-
s.description
|
20
|
-
s.author
|
21
|
-
s.email
|
22
|
-
s.homepage
|
23
|
-
s.
|
24
|
-
s.require_path
|
25
|
-
s.
|
26
|
-
s.
|
27
|
-
s.
|
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
|
-
|
35
|
-
|
36
|
-
|
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
data/ext/do_mysql_ext.c
ADDED
@@ -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
|
+
}
|