swift-db-mysql 0.1.0

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/CHANGELOG ADDED
@@ -0,0 +1,3 @@
1
+ == 0.1.0 (2012-07-20)
2
+
3
+ * Initial version.
data/README.md ADDED
@@ -0,0 +1,120 @@
1
+ # Swift MySQL adapter
2
+
3
+ MRI adapter for MySQL
4
+
5
+ ## Features
6
+
7
+ * Lightweight & fast
8
+ * Result typecasting
9
+ * Prepared statements, yeah finally!
10
+ * Asynchronous support
11
+ * Nested Transactions
12
+
13
+ ## API
14
+
15
+ ```
16
+ Swift::DB::Mysql
17
+ .new(options)
18
+ #execute(sql, *bind)
19
+ #prepare(sql)
20
+ #begin(savepoint = nil)
21
+ #commit(savepoint = nil)
22
+ #rollback(savepoint = nil)
23
+ #transaction(savepoint = nil, &block)
24
+ #close
25
+ #closed?
26
+ #escape(text)
27
+ #query(sql, *bind)
28
+ #fileno
29
+ #result
30
+ #write(table, fields = nil, io_or_string)
31
+
32
+ Swift::DB::MySql::Statement
33
+ .new(Swift::DB::Mysql, sql)
34
+ #execute(*bind)
35
+ #release
36
+
37
+ Swift::DB::Mysql::Result
38
+ #selected_rows
39
+ #affected_rows
40
+ #fields
41
+ #types
42
+ #each
43
+ #insert_id
44
+ ```
45
+
46
+ ## Example
47
+
48
+ ```ruby
49
+ require 'swift/db/mysql'
50
+
51
+ db = Swift::DB::Mysql.new(db: 'swift_test')
52
+
53
+ db.execute('drop table if exists users')
54
+ db.execute('create table users (id serial, name text, age integer, created_at datetime)')
55
+ db.execute('insert into users(name, age, created_at) values(?, ?, ?)', 'test', 30, Time.now.utc)
56
+
57
+ row = db.execute('select * from users').first
58
+ p row #=> {:id => 1, :name => 'test', :age => 30, :created_at=> #<Swift::DateTime>}
59
+ ```
60
+
61
+ ### Asynchronous
62
+
63
+ Hint: You can use `Adapter#fileno` and `EventMachine.watch` if you need to use this with EventMachine.
64
+
65
+ ```ruby
66
+ require 'swift/db/mysql'
67
+
68
+ rows = []
69
+ pool = 3.times.map {Swift::DB::Mysql.new(db: 'swift_test')}
70
+
71
+ 3.times do |n|
72
+ Thread.new do
73
+ pool[n].query("select sleep(#{(3 - n) / 10.0}), #{n + 1} as query_id") do |row|
74
+ rows << row[:query_id]
75
+ end
76
+ end
77
+ end
78
+
79
+ Thread.list.reject {|thread| Thread.current == thread}.each(&:join)
80
+ p rows #=> [3, 2, 1]
81
+ ```
82
+
83
+ ### Data I/O
84
+
85
+ The adapter supports data write via LOAD DATA LOCAL INFILE command.
86
+
87
+ ```ruby
88
+ require 'swift/db/mysql'
89
+
90
+ db = Swift::DB::Mysql.new(db: 'swift_test')
91
+ db.execute('drop table if exists users')
92
+ db.execute('create table users (id int auto_increment primary key, name text)')
93
+
94
+ db.write('users', %w{name}, "foo\nbar\nbaz\n")
95
+ db.write('users', %w{name}, StringIO.new("foo\nbar\nbaz\n"))
96
+ db.write('users', %w{name}, File.open("users.dat"))
97
+ ```
98
+
99
+ ## Performance
100
+
101
+ Don't read too much into it. Each library has its advantages and disadvantages.
102
+
103
+ ```
104
+ # insert 1000 rows and read them back 100 times
105
+
106
+ $ ruby check.rb
107
+ user system total real
108
+ do_mysql insert 0.170000 0.100000 0.270000 ( 0.629025)
109
+ do_mysql select 1.080000 0.130000 1.210000 ( 1.227585)
110
+
111
+ mysql2 insert 0.210000 0.040000 0.250000 ( 0.704030)
112
+ mysql2 select 0.940000 0.250000 1.190000 ( 1.206372)
113
+
114
+ swift insert 0.100000 0.060000 0.160000 ( 0.483229)
115
+ swift select 0.260000 0.010000 0.270000 ( 0.282307)
116
+ ```
117
+
118
+ ## License
119
+
120
+ MIT
@@ -0,0 +1,434 @@
1
+ // vim:ts=4:sts=4:sw=4:expandtab
2
+
3
+ // (c) Bharanee Rathna 2012
4
+
5
+ #include "adapter.h"
6
+ #include "typecast.h"
7
+
8
+ /* declaration */
9
+ VALUE cDMA, sUser;
10
+ VALUE db_mysql_result_each(VALUE);
11
+ VALUE db_mysql_result_allocate(VALUE);
12
+ VALUE db_mysql_result_load(VALUE, MYSQL_RES *, size_t, size_t);
13
+ VALUE db_mysql_statement_allocate(VALUE);
14
+ VALUE db_mysql_statement_initialize(VALUE, VALUE, VALUE);
15
+
16
+ /* definition */
17
+ Adapter* db_mysql_adapter_handle(VALUE self) {
18
+ Adapter *a;
19
+ Data_Get_Struct(self, Adapter, a);
20
+ if (!a)
21
+ rb_raise(eSwiftRuntimeError, "Invalid mysql adapter");
22
+ return a;
23
+ }
24
+
25
+ Adapter* db_mysql_adapter_handle_safe(VALUE self) {
26
+ Adapter *a = db_mysql_adapter_handle(self);
27
+ if (!a->connection)
28
+ rb_raise(eSwiftConnectionError, "mysql database is not open");
29
+ return a;
30
+ }
31
+
32
+ void db_mysql_adapter_mark(Adapter *a) {
33
+ if (a)
34
+ rb_gc_mark_maybe(a->io);
35
+ }
36
+
37
+ VALUE db_mysql_adapter_deallocate(Adapter *a) {
38
+ if (a && a->connection)
39
+ mysql_close(a->connection);
40
+ if (a)
41
+ free(a);
42
+ }
43
+
44
+ VALUE db_mysql_adapter_allocate(VALUE klass) {
45
+ Adapter *a = (Adapter*)malloc(sizeof(Adapter));
46
+
47
+ a->connection = 0;
48
+ a->t_nesting = 0;
49
+ a->io = Qnil;
50
+ return Data_Wrap_Struct(klass, db_mysql_adapter_mark, db_mysql_adapter_deallocate, a);
51
+ }
52
+
53
+ int db_mysql_adapter_infile_init(void **ptr, const char *filename, void *self) {
54
+ Adapter *a = db_mysql_adapter_handle_safe((VALUE)self);
55
+ *ptr = (void *)self;
56
+ return NIL_P(a->io) ? -1 : 0;
57
+ }
58
+
59
+ int db_mysql_adapter_infile_read(void *ptr, char *buffer, unsigned int size) {
60
+ VALUE data;
61
+ Adapter *a = db_mysql_adapter_handle_safe((VALUE)ptr);
62
+
63
+ data = rb_funcall(a->io, rb_intern("read"), 1, INT2NUM(size));
64
+
65
+ if (NIL_P(data)) return 0;
66
+
67
+ memcpy(buffer, RSTRING_PTR(data), RSTRING_LEN(data));
68
+ return RSTRING_LEN(data);
69
+ }
70
+
71
+ void db_mysql_adapter_infile_end(void *ptr) {
72
+ Adapter *a = db_mysql_adapter_handle_safe((VALUE)ptr);
73
+ a->io = Qnil;
74
+ }
75
+
76
+ int db_mysql_adapter_infile_error(void *ptr, char *error, unsigned int size) {
77
+ Adapter *a = db_mysql_adapter_handle_safe((VALUE)ptr);
78
+ a->io = Qnil;
79
+ snprintf(error, size, "error loading data using LOAD INFILE");
80
+ return 0;
81
+ }
82
+
83
+ char *ssl_option(VALUE ssl, char *key) {
84
+ VALUE option = rb_hash_aref(ssl, ID2SYM(rb_intern(key)));
85
+ return NIL_P(option) ? NULL : CSTRING(option);
86
+ }
87
+
88
+ VALUE db_mysql_adapter_initialize(VALUE self, VALUE options) {
89
+ char MYSQL_BOOL_TRUE = 1;
90
+ VALUE db, user, pass, host, port, ssl;
91
+ Adapter *a = db_mysql_adapter_handle(self);
92
+
93
+ if (TYPE(options) != T_HASH)
94
+ rb_raise(eSwiftArgumentError, "options needs to be a hash");
95
+
96
+ db = rb_hash_aref(options, ID2SYM(rb_intern("db")));
97
+ user = rb_hash_aref(options, ID2SYM(rb_intern("user")));
98
+ pass = rb_hash_aref(options, ID2SYM(rb_intern("pass")));
99
+ host = rb_hash_aref(options, ID2SYM(rb_intern("host")));
100
+ port = rb_hash_aref(options, ID2SYM(rb_intern("port")));
101
+ ssl = rb_hash_aref(options, ID2SYM(rb_intern("ssl")));
102
+
103
+ if (NIL_P(db))
104
+ rb_raise(eSwiftConnectionError, "Invalid db name");
105
+ if (NIL_P(host))
106
+ host = rb_str_new2("127.0.0.1");
107
+ if (NIL_P(port))
108
+ port = rb_str_new2("3306");
109
+ if (NIL_P(user))
110
+ user = sUser;
111
+
112
+ a->connection = mysql_init(0);
113
+ mysql_options(a->connection, MYSQL_OPT_RECONNECT, &MYSQL_BOOL_TRUE);
114
+ mysql_options(a->connection, MYSQL_OPT_LOCAL_INFILE, 0);
115
+
116
+ if (!NIL_P(ssl)) {
117
+ if (TYPE(ssl) != T_HASH)
118
+ rb_raise(eSwiftArgumentError, "ssl options needs to be a hash");
119
+
120
+ mysql_ssl_set(
121
+ a->connection,
122
+ ssl_option(ssl, "key"),
123
+ ssl_option(ssl, "cert"),
124
+ ssl_option(ssl, "ca"),
125
+ ssl_option(ssl, "capath"),
126
+ ssl_option(ssl, "cipher")
127
+ );
128
+ }
129
+
130
+ if (!mysql_real_connect(a->connection,
131
+ CSTRING(host), CSTRING(user), CSTRING(pass), CSTRING(db), atoi(CSTRING(port)), 0, CLIENT_FOUND_ROWS))
132
+ rb_raise(eSwiftConnectionError, "%s", mysql_error(a->connection));
133
+
134
+ mysql_set_character_set(a->connection, "utf8");
135
+ mysql_set_local_infile_handler(
136
+ a->connection,
137
+ db_mysql_adapter_infile_init, db_mysql_adapter_infile_read, db_mysql_adapter_infile_end, db_mysql_adapter_infile_error,
138
+ (void *)self
139
+ );
140
+ return self;
141
+ }
142
+
143
+ typedef struct Query {
144
+ MYSQL *connection;
145
+ VALUE sql;
146
+ } Query;
147
+
148
+ VALUE nogvl_mysql_adapter_execute(void *ptr) {
149
+ Query *q = (Query *)ptr;
150
+ return (VALUE)mysql_real_query(q->connection, RSTRING_PTR(q->sql), RSTRING_LEN(q->sql));
151
+ }
152
+
153
+ VALUE db_mysql_adapter_execute(int argc, VALUE *argv, VALUE self) {
154
+ VALUE sql, bind;
155
+ MYSQL_RES *result;
156
+ Adapter *a = db_mysql_adapter_handle_safe(self);
157
+ MYSQL *c = a->connection;
158
+
159
+ rb_scan_args(argc, argv, "10*", &sql, &bind);
160
+ sql = TO_S(sql);
161
+
162
+ if (RARRAY_LEN(bind) > 0)
163
+ sql = db_mysql_bind_sql(self, sql, bind);
164
+
165
+ Query q = {.connection = c, .sql = sql};
166
+
167
+ if ((int)rb_thread_blocking_region(nogvl_mysql_adapter_execute, &q, RUBY_UBF_IO, 0) != 0)
168
+ rb_raise(eSwiftRuntimeError, "%s", mysql_error(c));
169
+
170
+ result = mysql_store_result(c);
171
+ return db_mysql_result_load(db_mysql_result_allocate(cDMR), result, mysql_insert_id(c), mysql_affected_rows(c));
172
+ }
173
+
174
+ VALUE db_mysql_adapter_begin(int argc, VALUE *argv, VALUE self) {
175
+ char command[256];
176
+ VALUE savepoint;
177
+
178
+ Adapter *a = db_mysql_adapter_handle_safe(self);
179
+ rb_scan_args(argc, argv, "01", &savepoint);
180
+
181
+ if (a->t_nesting == 0) {
182
+ strcpy(command, "begin");
183
+ if (mysql_real_query(a->connection, command, strlen(command)) != 0)
184
+ rb_raise(eSwiftRuntimeError, "%s", mysql_error(a->connection));
185
+ a->t_nesting++;
186
+ if (NIL_P(savepoint))
187
+ return Qtrue;
188
+ }
189
+
190
+ if (NIL_P(savepoint))
191
+ savepoint = rb_uuid_string();
192
+
193
+ snprintf(command, 256, "savepoint %s", CSTRING(savepoint));
194
+ if (mysql_real_query(a->connection, command, strlen(command)) != 0)
195
+ rb_raise(eSwiftRuntimeError, "%s", mysql_error(a->connection));
196
+
197
+ a->t_nesting++;
198
+ return savepoint;
199
+ }
200
+
201
+ VALUE db_mysql_adapter_commit(int argc, VALUE *argv, VALUE self) {
202
+ VALUE savepoint;
203
+ char command[256];
204
+
205
+ Adapter *a = db_mysql_adapter_handle_safe(self);
206
+ rb_scan_args(argc, argv, "01", &savepoint);
207
+
208
+ if (a->t_nesting == 0)
209
+ return Qfalse;
210
+
211
+ if (NIL_P(savepoint)) {
212
+ strcpy(command, "commit");
213
+ if (mysql_real_query(a->connection, command, strlen(command)) != 0)
214
+ rb_raise(eSwiftRuntimeError, "%s", mysql_error(a->connection));
215
+ a->t_nesting--;
216
+ }
217
+ else {
218
+ snprintf(command, 256, "release savepoint %s", CSTRING(savepoint));
219
+ if (mysql_real_query(a->connection, command, strlen(command)) != 0)
220
+ rb_raise(eSwiftRuntimeError, "%s", mysql_error(a->connection));
221
+ a->t_nesting--;
222
+ }
223
+ return Qtrue;
224
+ }
225
+
226
+ VALUE db_mysql_adapter_rollback(int argc, VALUE *argv, VALUE self) {
227
+ VALUE savepoint;
228
+ char command[256];
229
+
230
+ Adapter *a = db_mysql_adapter_handle_safe(self);
231
+ rb_scan_args(argc, argv, "01", &savepoint);
232
+
233
+ if (a->t_nesting == 0)
234
+ return Qfalse;
235
+
236
+ if (NIL_P(savepoint)) {
237
+ strcpy(command, "rollback");
238
+ if (mysql_real_query(a->connection, command, strlen(command)) != 0)
239
+ rb_raise(eSwiftRuntimeError, "%s", mysql_error(a->connection));
240
+ a->t_nesting--;
241
+ }
242
+ else {
243
+ snprintf(command, 256, "rollback to savepoint %s", CSTRING(savepoint));
244
+ if (mysql_real_query(a->connection, command, strlen(command)) != 0)
245
+ rb_raise(eSwiftRuntimeError, "%s", mysql_error(a->connection));
246
+ a->t_nesting--;
247
+ }
248
+ return Qtrue;
249
+ }
250
+
251
+ VALUE db_mysql_adapter_transaction(int argc, VALUE *argv, VALUE self) {
252
+ int status;
253
+ VALUE savepoint, block, block_result;
254
+
255
+ Adapter *a = db_mysql_adapter_handle_safe(self);
256
+ rb_scan_args(argc, argv, "01&", &savepoint, &block);
257
+
258
+ if (NIL_P(block))
259
+ rb_raise(eSwiftRuntimeError, "mysql transaction requires a block");
260
+
261
+ if (a->t_nesting == 0) {
262
+ db_mysql_adapter_begin(1, &savepoint, self);
263
+ block_result = rb_protect(rb_yield, self, &status);
264
+ if (!status) {
265
+ db_mysql_adapter_commit(1, &savepoint, self);
266
+ if (!NIL_P(savepoint))
267
+ db_mysql_adapter_commit(0, 0, self);
268
+ }
269
+ else {
270
+ db_mysql_adapter_rollback(1, &savepoint, self);
271
+ if (!NIL_P(savepoint))
272
+ db_mysql_adapter_rollback(0, 0, self);
273
+ rb_jump_tag(status);
274
+ }
275
+ }
276
+ else {
277
+ if (NIL_P(savepoint))
278
+ savepoint = rb_uuid_string();
279
+ db_mysql_adapter_begin(1, &savepoint, self);
280
+ block_result = rb_protect(rb_yield, self, &status);
281
+ if (!status)
282
+ db_mysql_adapter_commit(1, &savepoint, self);
283
+ else {
284
+ db_mysql_adapter_rollback(1, &savepoint, self);
285
+ rb_jump_tag(status);
286
+ }
287
+ }
288
+
289
+ return block_result;
290
+ }
291
+
292
+ VALUE db_mysql_adapter_close(VALUE self) {
293
+ Adapter *a = db_mysql_adapter_handle(self);
294
+ if (a->connection) {
295
+ mysql_close(a->connection);
296
+ a->connection = 0;
297
+ return Qtrue;
298
+ }
299
+ return Qfalse;
300
+ }
301
+
302
+ VALUE db_mysql_adapter_closed_q(VALUE self) {
303
+ Adapter *a = db_mysql_adapter_handle(self);
304
+ return a->connection ? Qfalse : Qtrue;
305
+ }
306
+
307
+ VALUE db_mysql_adapter_prepare(VALUE self, VALUE sql) {
308
+ return db_mysql_statement_initialize(db_mysql_statement_allocate(cDMS), self, sql);
309
+ }
310
+
311
+ VALUE db_mysql_adapter_escape(VALUE self, VALUE fragment) {
312
+ VALUE text = TO_S(fragment);
313
+ char escaped[RSTRING_LEN(text) * 2 + 1];
314
+ Adapter *a = db_mysql_adapter_handle_safe(self);
315
+ mysql_real_escape_string(a->connection, escaped, RSTRING_PTR(text), RSTRING_LEN(text));
316
+ return rb_str_new2(escaped);
317
+ }
318
+
319
+ VALUE db_mysql_adapter_fileno(VALUE self) {
320
+ Adapter *a = db_mysql_adapter_handle_safe(self);
321
+ return INT2NUM(a->connection->net.fd);
322
+ }
323
+
324
+ VALUE db_mysql_adapter_query(int argc, VALUE *argv, VALUE self) {
325
+ VALUE sql, bind, result;
326
+ MYSQL_RES *r;
327
+ Adapter *a = db_mysql_adapter_handle_safe(self);
328
+ MYSQL *c = a->connection;
329
+
330
+ rb_scan_args(argc, argv, "10*", &sql, &bind);
331
+ sql = TO_S(sql);
332
+
333
+ if (RARRAY_LEN(bind) > 0)
334
+ sql = db_mysql_bind_sql(self, sql, bind);
335
+
336
+ mysql_send_query(c, RSTRING_PTR(sql), RSTRING_LEN(sql));
337
+
338
+ if (rb_block_given_p()) {
339
+ rb_thread_wait_fd(a->connection->net.fd);
340
+ if (mysql_read_query_result(c) != 0)
341
+ rb_raise(eSwiftRuntimeError, "%s", mysql_error(c));
342
+
343
+ r = mysql_store_result(c);
344
+ result = db_mysql_result_load(db_mysql_result_allocate(cDMR), r, mysql_insert_id(c), mysql_affected_rows(c));
345
+ return db_mysql_result_each(result);
346
+ }
347
+
348
+ return Qtrue;
349
+ }
350
+
351
+ VALUE db_mysql_adapter_result(VALUE self) {
352
+ MYSQL_RES *r;
353
+ Adapter *a = db_mysql_adapter_handle_safe(self);
354
+ MYSQL *c = a->connection;
355
+
356
+ if (mysql_read_query_result(c) != 0)
357
+ rb_raise(eSwiftRuntimeError, "%s", mysql_error(c));
358
+
359
+ r = mysql_store_result(c);
360
+ return db_mysql_result_load(db_mysql_result_allocate(cDMR), r, mysql_insert_id(c), mysql_affected_rows(c));
361
+ }
362
+
363
+ VALUE db_mysql_adapter_write(int argc, VALUE *argv, VALUE self) {
364
+ VALUE table, fields, io, data;
365
+
366
+ char *sql;
367
+ Adapter *a = db_mysql_adapter_handle_safe(self);
368
+ MYSQL *c = a->connection;
369
+
370
+ if (argc < 2 || argc > 3)
371
+ rb_raise(rb_eArgError, "wrong number of arguments (%d for 2..3)", argc);
372
+
373
+ table = fields = io = Qnil;
374
+ switch (argc) {
375
+ case 2:
376
+ table = argv[0];
377
+ io = argv[1];
378
+ break;
379
+ case 3:
380
+ table = argv[0];
381
+ fields = argv[1];
382
+ io = argv[2];
383
+ if (TYPE(fields) != T_ARRAY)
384
+ rb_raise(eSwiftArgumentError, "fields needs to be an array");
385
+ if (RARRAY_LEN(fields) < 1)
386
+ fields = Qnil;
387
+ }
388
+
389
+ if (argc > 1) {
390
+ sql = (char *)malloc(4096);
391
+ if (NIL_P(fields))
392
+ snprintf(sql, 4096, "load data local infile 'swift' replace into table %s", CSTRING(table));
393
+ else
394
+ snprintf(sql, 4096, "load data local infile 'swift' replace into table %s(%s)",
395
+ CSTRING(table), CSTRING(rb_ary_join(fields, rb_str_new2(", "))));
396
+
397
+ a->io = rb_respond_to(io, rb_intern("read")) ? io : rb_funcall(cStringIO, rb_intern("new"), 1, TO_S(io));
398
+ rb_gc_mark(a->io);
399
+ if (mysql_real_query(a->connection, sql, strlen(sql)) != 0) {
400
+ free(sql);
401
+ a->io = Qnil;
402
+ rb_raise(eSwiftRuntimeError, "%s", mysql_error(a->connection));
403
+ }
404
+
405
+ free(sql);
406
+ }
407
+
408
+ return db_mysql_result_load(db_mysql_result_allocate(cDMR), 0, mysql_insert_id(c), mysql_affected_rows(c));
409
+ }
410
+
411
+ void init_swift_db_mysql_adapter() {
412
+ rb_require("etc");
413
+ sUser = rb_funcall(CONST_GET(rb_mKernel, "Etc"), rb_intern("getlogin"), 0);
414
+ cDMA = rb_define_class_under(mDB, "Mysql", rb_cObject);
415
+
416
+ rb_define_alloc_func(cDMA, db_mysql_adapter_allocate);
417
+
418
+ rb_define_method(cDMA, "initialize", db_mysql_adapter_initialize, 1);
419
+ rb_define_method(cDMA, "execute", db_mysql_adapter_execute, -1);
420
+ rb_define_method(cDMA, "prepare", db_mysql_adapter_prepare, 1);
421
+ rb_define_method(cDMA, "begin", db_mysql_adapter_begin, -1);
422
+ rb_define_method(cDMA, "commit", db_mysql_adapter_commit, -1);
423
+ rb_define_method(cDMA, "rollback", db_mysql_adapter_rollback, -1);
424
+ rb_define_method(cDMA, "transaction", db_mysql_adapter_transaction, -1);
425
+ rb_define_method(cDMA, "close", db_mysql_adapter_close, 0);
426
+ rb_define_method(cDMA, "closed?", db_mysql_adapter_closed_q, 0);
427
+ rb_define_method(cDMA, "escape", db_mysql_adapter_escape, 1);
428
+ rb_define_method(cDMA, "fileno", db_mysql_adapter_fileno, 0);
429
+ rb_define_method(cDMA, "query", db_mysql_adapter_query, -1);
430
+ rb_define_method(cDMA, "result", db_mysql_adapter_result, 0);
431
+ rb_define_method(cDMA, "write", db_mysql_adapter_write, -1);
432
+
433
+ rb_global_variable(&sUser);
434
+ }