mysql2-sp 0.3.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/.gitignore +12 -0
  2. data/.rspec +3 -0
  3. data/.rvmrc +1 -0
  4. data/.travis.yml +7 -0
  5. data/CHANGELOG.md +230 -0
  6. data/Gemfile +3 -0
  7. data/MIT-LICENSE +20 -0
  8. data/README.md +350 -0
  9. data/Rakefile +5 -0
  10. data/benchmark/active_record.rb +51 -0
  11. data/benchmark/active_record_threaded.rb +42 -0
  12. data/benchmark/allocations.rb +33 -0
  13. data/benchmark/escape.rb +36 -0
  14. data/benchmark/query_with_mysql_casting.rb +80 -0
  15. data/benchmark/query_without_mysql_casting.rb +56 -0
  16. data/benchmark/sequel.rb +37 -0
  17. data/benchmark/setup_db.rb +119 -0
  18. data/benchmark/threaded.rb +44 -0
  19. data/examples/eventmachine.rb +21 -0
  20. data/examples/threaded.rb +20 -0
  21. data/ext/mysql2/client.c +955 -0
  22. data/ext/mysql2/client.h +42 -0
  23. data/ext/mysql2/extconf.rb +73 -0
  24. data/ext/mysql2/mysql2_ext.c +12 -0
  25. data/ext/mysql2/mysql2_ext.h +42 -0
  26. data/ext/mysql2/result.c +568 -0
  27. data/ext/mysql2/result.h +20 -0
  28. data/ext/mysql2/wait_for_single_fd.h +36 -0
  29. data/lib/mysql2.rb +21 -0
  30. data/lib/mysql2/client.rb +242 -0
  31. data/lib/mysql2/em.rb +37 -0
  32. data/lib/mysql2/error.rb +15 -0
  33. data/lib/mysql2/result.rb +5 -0
  34. data/lib/mysql2/version.rb +3 -0
  35. data/mysql2.gemspec +29 -0
  36. data/spec/em/em_spec.rb +50 -0
  37. data/spec/mysql2/client_spec.rb +491 -0
  38. data/spec/mysql2/error_spec.rb +69 -0
  39. data/spec/mysql2/result_spec.rb +388 -0
  40. data/spec/rcov.opts +3 -0
  41. data/spec/spec_helper.rb +67 -0
  42. data/tasks/benchmarks.rake +20 -0
  43. data/tasks/compile.rake +71 -0
  44. data/tasks/rspec.rake +16 -0
  45. data/tasks/vendor_mysql.rake +40 -0
  46. metadata +198 -0
@@ -0,0 +1,491 @@
1
+ # encoding: UTF-8
2
+ require 'spec_helper'
3
+
4
+ describe Mysql2::Client do
5
+ before(:each) do
6
+ @client = Mysql2::Client.new
7
+ end
8
+
9
+ if defined? Encoding
10
+ it "should raise an exception on create for invalid encodings" do
11
+ lambda {
12
+ c = Mysql2::Client.new(:encoding => "fake")
13
+ }.should raise_error(Mysql2::Error)
14
+ end
15
+ end
16
+
17
+ it "should accept connect flags and pass them to #connect" do
18
+ klient = Class.new(Mysql2::Client) do
19
+ attr_reader :connect_args
20
+ def connect *args
21
+ @connect_args ||= []
22
+ @connect_args << args
23
+ end
24
+ end
25
+ client = klient.new :flags => Mysql2::Client::FOUND_ROWS
26
+ (client.connect_args.last.last & Mysql2::Client::FOUND_ROWS).should be_true
27
+ end
28
+
29
+ it "should default flags to (REMEMBER_OPTIONS, LONG_PASSWORD, LONG_FLAG, TRANSACTIONS, PROTOCOL_41, SECURE_CONNECTION)" do
30
+ klient = Class.new(Mysql2::Client) do
31
+ attr_reader :connect_args
32
+ def connect *args
33
+ @connect_args ||= []
34
+ @connect_args << args
35
+ end
36
+ end
37
+ client = klient.new
38
+ (client.connect_args.last.last & (Mysql2::Client::REMEMBER_OPTIONS |
39
+ Mysql2::Client::LONG_PASSWORD |
40
+ Mysql2::Client::LONG_FLAG |
41
+ Mysql2::Client::TRANSACTIONS |
42
+ Mysql2::Client::PROTOCOL_41 |
43
+ Mysql2::Client::SECURE_CONNECTION)).should be_true
44
+ end
45
+
46
+ it "should have a global default_query_options hash" do
47
+ Mysql2::Client.should respond_to(:default_query_options)
48
+ end
49
+
50
+ it "should be able to connect via SSL options" do
51
+ pending("DON'T WORRY, THIS TEST PASSES :) - but is machine-specific. You need to have MySQL running with SSL configured and enabled. Then update the paths in this test to your needs and remove the pending state.")
52
+ ssl_client = nil
53
+ lambda {
54
+ ssl_client = Mysql2::Client.new(
55
+ :sslkey => '/path/to/client-key.pem',
56
+ :sslcert => '/path/to/client-cert.pem',
57
+ :sslca => '/path/to/ca-cert.pem',
58
+ :sslcapath => '/path/to/newcerts/',
59
+ :sslcipher => 'DHE-RSA-AES256-SHA'
60
+ )
61
+ }.should_not raise_error(Mysql2::Error)
62
+
63
+ results = ssl_client.query("SHOW STATUS WHERE Variable_name = \"Ssl_version\" OR Variable_name = \"Ssl_cipher\"").to_a
64
+ results[0]['Variable_name'].should eql('Ssl_cipher')
65
+ results[0]['Value'].should_not be_nil
66
+ results[0]['Value'].class.should eql(String)
67
+
68
+ results[1]['Variable_name'].should eql('Ssl_version')
69
+ results[1]['Value'].should_not be_nil
70
+ results[1]['Value'].class.should eql(String)
71
+ end
72
+
73
+ it "should respond to #close" do
74
+ @client.should respond_to(:close)
75
+ end
76
+
77
+ it "should be able to close properly" do
78
+ @client.close.should be_nil
79
+ lambda {
80
+ @client.query "SELECT 1"
81
+ }.should raise_error(Mysql2::Error)
82
+ end
83
+
84
+ it "should respond to #query" do
85
+ @client.should respond_to(:query)
86
+ end
87
+
88
+ it "should expect read_timeout to be a positive integer" do
89
+ lambda {
90
+ Mysql2::Client.new(:read_timeout => -1)
91
+ }.should raise_error(Mysql2::Error)
92
+ end
93
+
94
+ context "#query" do
95
+ it "should only accept strings as the query parameter" do
96
+ lambda {
97
+ @client.query ["SELECT 'not right'"]
98
+ }.should raise_error(TypeError)
99
+ end
100
+
101
+ it "should accept an options hash that inherits from Mysql2::Client.default_query_options" do
102
+ @client.query "SELECT 1", :something => :else
103
+ @client.query_options.should eql(@client.query_options.merge(:something => :else))
104
+ end
105
+
106
+ it "should return results as a hash by default" do
107
+ @client.query("SELECT 1").first.class.should eql(Hash)
108
+ end
109
+
110
+ it "should be able to return results as an array" do
111
+ @client.query("SELECT 1", :as => :array).first.class.should eql(Array)
112
+ @client.query("SELECT 1").each(:as => :array)
113
+ end
114
+
115
+ it "should be able to return results with symbolized keys" do
116
+ @client.query("SELECT 1", :symbolize_keys => true).first.keys[0].class.should eql(Symbol)
117
+ end
118
+
119
+ it "should require an open connection" do
120
+ @client.close
121
+ lambda {
122
+ @client.query "SELECT 1"
123
+ }.should raise_error(Mysql2::Error)
124
+ end
125
+
126
+ if RUBY_PLATFORM !~ /mingw|mswin/
127
+ it "should not allow another query to be sent without fetching a result first" do
128
+ @client.query("SELECT 1", :async => true)
129
+ lambda {
130
+ @client.query("SELECT 1")
131
+ }.should raise_error(Mysql2::Error)
132
+ end
133
+
134
+ it "should timeout if we wait longer than :read_timeout" do
135
+ client = Mysql2::Client.new(:read_timeout => 1)
136
+ lambda {
137
+ client.query("SELECT sleep(2)")
138
+ }.should raise_error(Mysql2::Error)
139
+ end
140
+
141
+ # XXX this test is not deterministic (because Unix signal handling is not)
142
+ # and may fail on a loaded system
143
+ it "should run signal handlers while waiting for a response" do
144
+ mark = {}
145
+ trap(:USR1) { mark[:USR1] = Time.now }
146
+ begin
147
+ mark[:START] = Time.now
148
+ pid = fork do
149
+ sleep 1 # wait for client "SELECT sleep(2)" query to start
150
+ Process.kill(:USR1, Process.ppid)
151
+ sleep # wait for explicit kill to prevent GC disconnect
152
+ end
153
+ @client.query("SELECT sleep(2)")
154
+ mark[:END] = Time.now
155
+ mark.include?(:USR1).should be_true
156
+ (mark[:USR1] - mark[:START]).should >= 1
157
+ (mark[:USR1] - mark[:START]).should < 1.1
158
+ (mark[:END] - mark[:USR1]).should > 0.9
159
+ (mark[:END] - mark[:START]).should >= 2
160
+ (mark[:END] - mark[:START]).should < 2.1
161
+ Process.kill(:TERM, pid)
162
+ Process.waitpid2(pid)
163
+ ensure
164
+ trap(:USR1, 'DEFAULT')
165
+ end
166
+ end
167
+
168
+ it "#socket should return a Fixnum (file descriptor from C)" do
169
+ @client.socket.class.should eql(Fixnum)
170
+ @client.socket.should_not eql(0)
171
+ end
172
+
173
+ it "#socket should require an open connection" do
174
+ @client.close
175
+ lambda {
176
+ @client.socket
177
+ }.should raise_error(Mysql2::Error)
178
+ end
179
+
180
+ it "should close the connection when an exception is raised" do
181
+ begin
182
+ Timeout.timeout(1) do
183
+ @client.query("SELECT sleep(2)")
184
+ end
185
+ rescue Timeout::Error
186
+ end
187
+
188
+ lambda {
189
+ @client.query("SELECT 1")
190
+ }.should raise_error(Mysql2::Error, 'closed MySQL connection')
191
+ end
192
+
193
+ it "should handle Timeouts without leaving the connection hanging if reconnect is true" do
194
+ client = Mysql2::Client.new(:reconnect => true)
195
+ begin
196
+ Timeout.timeout(1) do
197
+ client.query("SELECT sleep(2)")
198
+ end
199
+ rescue Timeout::Error
200
+ end
201
+
202
+ lambda {
203
+ client.query("SELECT 1")
204
+ }.should_not raise_error(Mysql2::Error)
205
+ end
206
+
207
+ it "threaded queries should be supported" do
208
+ threads, results = [], {}
209
+ connect = lambda{ Mysql2::Client.new(:host => "localhost", :username => "root") }
210
+ Timeout.timeout(0.7) do
211
+ 5.times {
212
+ threads << Thread.new do
213
+ results[Thread.current.object_id] = connect.call.query("SELECT sleep(0.5) as result")
214
+ end
215
+ }
216
+ end
217
+ threads.each{|t| t.join }
218
+ results.keys.sort.should eql(threads.map{|t| t.object_id }.sort)
219
+ end
220
+
221
+ it "evented async queries should be supported" do
222
+ # should immediately return nil
223
+ @client.query("SELECT sleep(0.1)", :async => true).should eql(nil)
224
+
225
+ io_wrapper = IO.for_fd(@client.socket)
226
+ loops = 0
227
+ loop do
228
+ if IO.select([io_wrapper], nil, nil, 0.05)
229
+ break
230
+ else
231
+ loops += 1
232
+ end
233
+ end
234
+
235
+ # make sure we waited some period of time
236
+ (loops >= 1).should be_true
237
+
238
+ result = @client.async_result
239
+ result.class.should eql(Mysql2::Result)
240
+ end
241
+ end
242
+
243
+ context "Multiple results sets" do
244
+ before(:each) do
245
+ @multi_client = Mysql2::Client.new( :flags => Mysql2::Client::MULTI_STATEMENTS)
246
+ end
247
+
248
+ it "returns multiple result sets" do
249
+ @multi_client.query( "select 1 as 'set_1'; select 2 as 'set_2'").first.should == { 'set_1' => 1 }
250
+
251
+ @multi_client.next_result.should == true
252
+ @multi_client.store_result.first.should == { 'set_2' => 2 }
253
+
254
+ @multi_client.next_result.should == false
255
+ end
256
+
257
+ it "does not interfere with other statements" do
258
+ @multi_client.query( "select 1 as 'set_1'; select 2 as 'set_2'")
259
+ while( @multi_client.next_result )
260
+ @multi_client.store_result
261
+ end
262
+
263
+ @multi_client.query( "select 3 as 'next'").first.should == { 'next' => 3 }
264
+ end
265
+ end
266
+
267
+
268
+ end
269
+
270
+ it "should respond to #socket" do
271
+ @client.should respond_to(:socket)
272
+ end
273
+
274
+ if RUBY_PLATFORM =~ /mingw|mswin/
275
+ it "#socket should raise as it's not supported" do
276
+ lambda {
277
+ @client.socket
278
+ }.should raise_error(Mysql2::Error)
279
+ end
280
+ end
281
+
282
+ it "should respond to escape" do
283
+ Mysql2::Client.should respond_to(:escape)
284
+ end
285
+
286
+ context "escape" do
287
+ it "should return a new SQL-escape version of the passed string" do
288
+ Mysql2::Client.escape("abc'def\"ghi\0jkl%mno").should eql("abc\\'def\\\"ghi\\0jkl%mno")
289
+ end
290
+
291
+ it "should return the passed string if nothing was escaped" do
292
+ str = "plain"
293
+ Mysql2::Client.escape(str).object_id.should eql(str.object_id)
294
+ end
295
+
296
+ it "should not overflow the thread stack" do
297
+ lambda {
298
+ Thread.new { Mysql2::Client.escape("'" * 256 * 1024) }.join
299
+ }.should_not raise_error(SystemStackError)
300
+ end
301
+
302
+ it "should not overflow the process stack" do
303
+ lambda {
304
+ Thread.new { Mysql2::Client.escape("'" * 1024 * 1024 * 4) }.join
305
+ }.should_not raise_error(SystemStackError)
306
+ end
307
+
308
+ if RUBY_VERSION =~ /1.9/
309
+ it "should carry over the original string's encoding" do
310
+ str = "abc'def\"ghi\0jkl%mno"
311
+ escaped = Mysql2::Client.escape(str)
312
+ escaped.encoding.should eql(str.encoding)
313
+
314
+ str.encode!('us-ascii')
315
+ escaped = Mysql2::Client.escape(str)
316
+ escaped.encoding.should eql(str.encoding)
317
+ end
318
+ end
319
+ end
320
+
321
+ it "should respond to #escape" do
322
+ @client.should respond_to(:escape)
323
+ end
324
+
325
+ context "#escape" do
326
+ it "should return a new SQL-escape version of the passed string" do
327
+ @client.escape("abc'def\"ghi\0jkl%mno").should eql("abc\\'def\\\"ghi\\0jkl%mno")
328
+ end
329
+
330
+ it "should return the passed string if nothing was escaped" do
331
+ str = "plain"
332
+ @client.escape(str).object_id.should eql(str.object_id)
333
+ end
334
+
335
+ it "should not overflow the thread stack" do
336
+ lambda {
337
+ Thread.new { @client.escape("'" * 256 * 1024) }.join
338
+ }.should_not raise_error(SystemStackError)
339
+ end
340
+
341
+ it "should not overflow the process stack" do
342
+ lambda {
343
+ Thread.new { @client.escape("'" * 1024 * 1024 * 4) }.join
344
+ }.should_not raise_error(SystemStackError)
345
+ end
346
+
347
+ it "should require an open connection" do
348
+ @client.close
349
+ lambda {
350
+ @client.escape ""
351
+ }.should raise_error(Mysql2::Error)
352
+ end
353
+ end
354
+
355
+ it "should respond to #info" do
356
+ @client.should respond_to(:info)
357
+ end
358
+
359
+ it "#info should return a hash containing the client version ID and String" do
360
+ info = @client.info
361
+ info.class.should eql(Hash)
362
+ info.should have_key(:id)
363
+ info[:id].class.should eql(Fixnum)
364
+ info.should have_key(:version)
365
+ info[:version].class.should eql(String)
366
+ end
367
+
368
+ if defined? Encoding
369
+ context "strings returned by #info" do
370
+ it "should default to the connection's encoding if Encoding.default_internal is nil" do
371
+ Encoding.default_internal = nil
372
+ @client.info[:version].encoding.should eql(Encoding.find('utf-8'))
373
+
374
+ client2 = Mysql2::Client.new :encoding => 'ascii'
375
+ client2.info[:version].encoding.should eql(Encoding.find('us-ascii'))
376
+ end
377
+
378
+ it "should use Encoding.default_internal" do
379
+ Encoding.default_internal = Encoding.find('utf-8')
380
+ @client.info[:version].encoding.should eql(Encoding.default_internal)
381
+ Encoding.default_internal = Encoding.find('us-ascii')
382
+ @client.info[:version].encoding.should eql(Encoding.default_internal)
383
+ end
384
+ end
385
+ end
386
+
387
+ it "should respond to #server_info" do
388
+ @client.should respond_to(:server_info)
389
+ end
390
+
391
+ it "#server_info should return a hash containing the client version ID and String" do
392
+ server_info = @client.server_info
393
+ server_info.class.should eql(Hash)
394
+ server_info.should have_key(:id)
395
+ server_info[:id].class.should eql(Fixnum)
396
+ server_info.should have_key(:version)
397
+ server_info[:version].class.should eql(String)
398
+ end
399
+
400
+ it "#server_info should require an open connection" do
401
+ @client.close
402
+ lambda {
403
+ @client.server_info
404
+ }.should raise_error(Mysql2::Error)
405
+ end
406
+
407
+ if defined? Encoding
408
+ context "strings returned by #server_info" do
409
+ it "should default to the connection's encoding if Encoding.default_internal is nil" do
410
+ Encoding.default_internal = nil
411
+ @client.server_info[:version].encoding.should eql(Encoding.find('utf-8'))
412
+
413
+ client2 = Mysql2::Client.new :encoding => 'ascii'
414
+ client2.server_info[:version].encoding.should eql(Encoding.find('us-ascii'))
415
+ end
416
+
417
+ it "should use Encoding.default_internal" do
418
+ Encoding.default_internal = Encoding.find('utf-8')
419
+ @client.server_info[:version].encoding.should eql(Encoding.default_internal)
420
+ Encoding.default_internal = Encoding.find('us-ascii')
421
+ @client.server_info[:version].encoding.should eql(Encoding.default_internal)
422
+ end
423
+ end
424
+ end
425
+
426
+ it "should raise a Mysql2::Error exception upon connection failure" do
427
+ lambda {
428
+ bad_client = Mysql2::Client.new :host => "localhost", :username => 'asdfasdf8d2h', :password => 'asdfasdfw42'
429
+ }.should raise_error(Mysql2::Error)
430
+
431
+ lambda {
432
+ good_client = Mysql2::Client.new
433
+ }.should_not raise_error(Mysql2::Error)
434
+ end
435
+
436
+ context 'write operations api' do
437
+ before(:each) do
438
+ @client.query "USE test"
439
+ @client.query "CREATE TABLE IF NOT EXISTS lastIdTest (`id` int(11) NOT NULL AUTO_INCREMENT, blah INT(11), PRIMARY KEY (`id`))"
440
+ end
441
+
442
+ after(:each) do
443
+ @client.query "DROP TABLE lastIdTest"
444
+ end
445
+
446
+ it "should respond to #last_id" do
447
+ @client.should respond_to(:last_id)
448
+ end
449
+
450
+ it "#last_id should return a Fixnum, the from the last INSERT/UPDATE" do
451
+ @client.last_id.should eql(0)
452
+ @client.query "INSERT INTO lastIdTest (blah) VALUES (1234)"
453
+ @client.last_id.should eql(1)
454
+ end
455
+
456
+ it "should respond to #last_id" do
457
+ @client.should respond_to(:last_id)
458
+ end
459
+
460
+ it "#last_id should return a Fixnum, the from the last INSERT/UPDATE" do
461
+ @client.query "INSERT INTO lastIdTest (blah) VALUES (1234)"
462
+ @client.affected_rows.should eql(1)
463
+ @client.query "UPDATE lastIdTest SET blah=4321 WHERE id=1"
464
+ @client.affected_rows.should eql(1)
465
+ end
466
+ end
467
+
468
+ it "should respond to #thread_id" do
469
+ @client.should respond_to(:thread_id)
470
+ end
471
+
472
+ it "#thread_id should be a Fixnum" do
473
+ @client.thread_id.class.should eql(Fixnum)
474
+ end
475
+
476
+ it "should respond to #ping" do
477
+ @client.should respond_to(:ping)
478
+ end
479
+
480
+ it "#thread_id should return a boolean" do
481
+ @client.ping.should eql(true)
482
+ @client.close
483
+ @client.ping.should eql(false)
484
+ end
485
+
486
+ if RUBY_VERSION =~ /1.9/
487
+ it "should respond to #encoding" do
488
+ @client.should respond_to(:encoding)
489
+ end
490
+ end
491
+ end