mysql2 0.2.21 → 0.2.22
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +65 -25
- data/ext/mysql2/client.c +20 -15
- data/ext/mysql2/client.h +4 -2
- data/ext/mysql2/infile.c +119 -0
- data/ext/mysql2/infile.h +1 -0
- data/ext/mysql2/mysql2_ext.h +1 -0
- data/ext/mysql2/result.c +14 -13
- data/lib/mysql2/error.rb +72 -6
- data/lib/mysql2/version.rb +1 -1
- data/spec/mysql2/client_spec.rb +114 -20
- data/spec/mysql2/error_spec.rb +57 -47
- data/spec/mysql2/result_spec.rb +131 -94
- data/spec/spec_helper.rb +9 -0
- data/spec/test_data +1 -0
- metadata +6 -2
data/lib/mysql2/error.rb
CHANGED
@@ -1,15 +1,81 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
|
1
3
|
module Mysql2
|
2
4
|
class Error < StandardError
|
3
|
-
|
5
|
+
REPLACEMENT_CHAR = '?'
|
6
|
+
ENCODE_OPTS = {:undef => :replace, :invalid => :replace, :replace => REPLACEMENT_CHAR}
|
4
7
|
|
5
|
-
|
6
|
-
|
7
|
-
@error_number = nil
|
8
|
-
@sql_state = nil
|
9
|
-
end
|
8
|
+
attr_accessor :error_number, :sql_state
|
9
|
+
attr_writer :server_version
|
10
10
|
|
11
11
|
# Mysql gem compatibility
|
12
12
|
alias_method :errno, :error_number
|
13
13
|
alias_method :error, :message
|
14
|
+
|
15
|
+
def initialize(msg, server_version=nil)
|
16
|
+
self.server_version = server_version
|
17
|
+
|
18
|
+
super(clean_message(msg))
|
19
|
+
end
|
20
|
+
|
21
|
+
if "".respond_to? :encode
|
22
|
+
def sql_state=(state)
|
23
|
+
@sql_state = state.encode(ENCODE_OPTS)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
# In MySQL 5.5+ error messages are always constructed server-side as UTF-8
|
30
|
+
# then returned in the encoding set by the `character_set_results` system
|
31
|
+
# variable.
|
32
|
+
#
|
33
|
+
# See http://dev.mysql.com/doc/refman/5.5/en/charset-errors.html for
|
34
|
+
# more contetx.
|
35
|
+
#
|
36
|
+
# Before MySQL 5.5 error message template strings are in whatever encoding
|
37
|
+
# is associated with the error message language.
|
38
|
+
# See http://dev.mysql.com/doc/refman/5.1/en/error-message-language.html
|
39
|
+
# for more information.
|
40
|
+
#
|
41
|
+
# The issue is that the user-data inserted in the message could potentially
|
42
|
+
# be in any encoding MySQL supports and is insert into the latin1, euckr or
|
43
|
+
# koi8r string raw. Meaning there's a high probability the string will be
|
44
|
+
# corrupt encoding-wise.
|
45
|
+
#
|
46
|
+
# See http://dev.mysql.com/doc/refman/5.1/en/charset-errors.html for
|
47
|
+
# more information.
|
48
|
+
#
|
49
|
+
# So in an attempt to make sure the error message string is always in a valid
|
50
|
+
# encoding, we'll assume UTF-8 and clean the string of anything that's not a
|
51
|
+
# valid UTF-8 character.
|
52
|
+
#
|
53
|
+
# Except for if we're on 1.8, where we'll do nothing ;)
|
54
|
+
#
|
55
|
+
# Returns a valid UTF-8 string in Ruby 1.9+, the original string on Ruby 1.8
|
56
|
+
def clean_message(message)
|
57
|
+
return message if !message.respond_to?(:encoding)
|
58
|
+
|
59
|
+
if @server_version && @server_version > 50500
|
60
|
+
message.encode(ENCODE_OPTS)
|
61
|
+
else
|
62
|
+
if message.respond_to? :scrub
|
63
|
+
message.scrub(REPLACEMENT_CHAR).encode(ENCODE_OPTS)
|
64
|
+
else
|
65
|
+
# This is ugly as hell but Ruby 1.9 doesn't provide a way to clean a string
|
66
|
+
# and retain it's valid UTF-8 characters, that I know of.
|
67
|
+
|
68
|
+
new_message = "".force_encoding(Encoding::UTF_8)
|
69
|
+
message.chars.each do |char|
|
70
|
+
if char.valid_encoding?
|
71
|
+
new_message << char
|
72
|
+
else
|
73
|
+
new_message << REPLACEMENT_CHAR
|
74
|
+
end
|
75
|
+
end
|
76
|
+
new_message.encode(ENCODE_OPTS)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
14
80
|
end
|
15
81
|
end
|
data/lib/mysql2/version.rb
CHANGED
data/spec/mysql2/client_spec.rb
CHANGED
@@ -7,13 +7,14 @@ describe Mysql2::Client do
|
|
7
7
|
|
8
8
|
it "should not raise an exception for valid defaults group" do
|
9
9
|
lambda {
|
10
|
-
|
10
|
+
opts = DatabaseCredentials['root'].merge(:default_file => cnf_file, :default_group => "test")
|
11
|
+
@client = Mysql2::Client.new(opts)
|
11
12
|
}.should_not raise_error(Mysql2::Error)
|
12
13
|
end
|
13
14
|
|
14
15
|
it "should not raise an exception without default group" do
|
15
16
|
lambda {
|
16
|
-
@client = Mysql2::Client.new(:default_file => cnf_file)
|
17
|
+
@client = Mysql2::Client.new(DatabaseCredentials['root'].merge(:default_file => cnf_file))
|
17
18
|
}.should_not raise_error(Mysql2::Error)
|
18
19
|
end
|
19
20
|
end
|
@@ -78,7 +79,10 @@ describe Mysql2::Client do
|
|
78
79
|
end
|
79
80
|
|
80
81
|
it "should be able to connect via SSL options" do
|
81
|
-
|
82
|
+
ssl = @client.query "SHOW VARIABLES LIKE 'have_%ssl'"
|
83
|
+
ssl_enabled = ssl.any? {|x| x['Value'] == 'ENABLED'}
|
84
|
+
pending("DON'T WORRY, THIS TEST PASSES - but SSL is not enabled in your MySQL daemon.") unless ssl_enabled
|
85
|
+
pending("DON'T WORRY, THIS TEST PASSES - but you must update the SSL cert paths in this test and remove this pending state.")
|
82
86
|
ssl_client = nil
|
83
87
|
lambda {
|
84
88
|
ssl_client = Mysql2::Client.new(
|
@@ -104,6 +108,24 @@ describe Mysql2::Client do
|
|
104
108
|
ssl_client.close
|
105
109
|
end
|
106
110
|
|
111
|
+
it "should not leave dangling connections after garbage collection" do
|
112
|
+
GC.start
|
113
|
+
sleep 0.300 # Let GC do its work
|
114
|
+
client = Mysql2::Client.new(DatabaseCredentials['root'])
|
115
|
+
before_count = client.query("SHOW STATUS LIKE 'Threads_connected'").first['Value'].to_i
|
116
|
+
|
117
|
+
10.times do
|
118
|
+
Mysql2::Client.new(DatabaseCredentials['root']).query('SELECT 1')
|
119
|
+
end
|
120
|
+
after_count = client.query("SHOW STATUS LIKE 'Threads_connected'").first['Value'].to_i
|
121
|
+
after_count.should == before_count + 10
|
122
|
+
|
123
|
+
GC.start
|
124
|
+
sleep 0.300 # Let GC do its work
|
125
|
+
final_count = client.query("SHOW STATUS LIKE 'Threads_connected'").first['Value'].to_i
|
126
|
+
final_count.should == before_count
|
127
|
+
end
|
128
|
+
|
107
129
|
it "should be able to connect to database with numeric-only name" do
|
108
130
|
lambda {
|
109
131
|
creds = DatabaseCredentials['numericuser']
|
@@ -179,6 +201,49 @@ describe Mysql2::Client do
|
|
179
201
|
end
|
180
202
|
end
|
181
203
|
|
204
|
+
context ":local_infile" do
|
205
|
+
before(:all) do
|
206
|
+
@client_i = Mysql2::Client.new DatabaseCredentials['root'].merge(:local_infile => true)
|
207
|
+
local = @client_i.query "SHOW VARIABLES LIKE 'local_infile'"
|
208
|
+
local_enabled = local.any? {|x| x['Value'] == 'ON'}
|
209
|
+
pending("DON'T WORRY, THIS TEST PASSES - but LOCAL INFILE is not enabled in your MySQL daemon.") unless local_enabled
|
210
|
+
|
211
|
+
@client_i.query %[
|
212
|
+
CREATE TABLE IF NOT EXISTS infileTest (
|
213
|
+
id MEDIUMINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
214
|
+
foo VARCHAR(10),
|
215
|
+
bar MEDIUMTEXT
|
216
|
+
)
|
217
|
+
]
|
218
|
+
end
|
219
|
+
|
220
|
+
after(:all) do
|
221
|
+
@client_i.query "DROP TABLE infileTest"
|
222
|
+
end
|
223
|
+
|
224
|
+
it "should raise an error when local_infile is disabled" do
|
225
|
+
client = Mysql2::Client.new DatabaseCredentials['root'].merge(:local_infile => false)
|
226
|
+
lambda {
|
227
|
+
client.query "LOAD DATA LOCAL INFILE 'spec/test_data' INTO TABLE infileTest"
|
228
|
+
}.should raise_error(Mysql2::Error, %r{command is not allowed})
|
229
|
+
end
|
230
|
+
|
231
|
+
it "should raise an error when a non-existent file is loaded" do
|
232
|
+
lambda {
|
233
|
+
@client_i.query "LOAD DATA LOCAL INFILE 'this/file/is/not/here' INTO TABLE infileTest"
|
234
|
+
}.should_not raise_error(Mysql2::Error, %r{file not found: this/file/is/not/here})
|
235
|
+
end
|
236
|
+
|
237
|
+
it "should LOAD DATA LOCAL INFILE" do
|
238
|
+
@client_i.query "LOAD DATA LOCAL INFILE 'spec/test_data' INTO TABLE infileTest"
|
239
|
+
info = @client_i.query_info
|
240
|
+
info.should eql({:records => 1, :deleted => 0, :skipped => 0, :warnings => 0})
|
241
|
+
|
242
|
+
result = @client_i.query "SELECT * FROM infileTest"
|
243
|
+
result.first.should eql({'id' => 1, 'foo' => 'Hello', 'bar' => 'World'})
|
244
|
+
end
|
245
|
+
end
|
246
|
+
|
182
247
|
it "should expect connect_timeout to be a positive integer" do
|
183
248
|
lambda {
|
184
249
|
Mysql2::Client.new(:connect_timeout => -1)
|
@@ -332,6 +397,7 @@ describe Mysql2::Client do
|
|
332
397
|
end
|
333
398
|
|
334
399
|
it "should close the connection when an exception is raised" do
|
400
|
+
pending "Ruby 2.1 has changed Timeout behavior." if RUBY_VERSION =~ /2.1/
|
335
401
|
begin
|
336
402
|
Timeout.timeout(1) do
|
337
403
|
@client.query("SELECT sleep(2)")
|
@@ -345,6 +411,7 @@ describe Mysql2::Client do
|
|
345
411
|
end
|
346
412
|
|
347
413
|
it "should handle Timeouts without leaving the connection hanging if reconnect is true" do
|
414
|
+
pending "Ruby 2.1 has changed Timeout behavior." if RUBY_VERSION =~ /2.1/
|
348
415
|
client = Mysql2::Client.new(DatabaseCredentials['root'].merge(:reconnect => true))
|
349
416
|
begin
|
350
417
|
Timeout.timeout(1) do
|
@@ -359,6 +426,7 @@ describe Mysql2::Client do
|
|
359
426
|
end
|
360
427
|
|
361
428
|
it "should handle Timeouts without leaving the connection hanging if reconnect is set to true after construction true" do
|
429
|
+
pending "Ruby 2.1 has changed Timeout behavior." if RUBY_VERSION =~ /2.1/
|
362
430
|
client = Mysql2::Client.new(DatabaseCredentials['root'])
|
363
431
|
begin
|
364
432
|
Timeout.timeout(1) do
|
@@ -433,6 +501,15 @@ describe Mysql2::Client do
|
|
433
501
|
@multi_client = Mysql2::Client.new(DatabaseCredentials['root'].merge(:flags => Mysql2::Client::MULTI_STATEMENTS))
|
434
502
|
end
|
435
503
|
|
504
|
+
it "should raise an exception when one of multiple statements fails" do
|
505
|
+
result = @multi_client.query("SELECT 1 as 'set_1'; SELECT * FROM invalid_table_name;SELECT 2 as 'set_2';")
|
506
|
+
result.first['set_1'].should be(1)
|
507
|
+
lambda {
|
508
|
+
@multi_client.next_result
|
509
|
+
}.should raise_error(Mysql2::Error)
|
510
|
+
@multi_client.next_result.should be_false
|
511
|
+
end
|
512
|
+
|
436
513
|
it "returns multiple result sets" do
|
437
514
|
@multi_client.query( "select 1 as 'set_1'; select 2 as 'set_2'").first.should eql({ 'set_1' => 1 })
|
438
515
|
|
@@ -579,18 +656,22 @@ describe Mysql2::Client do
|
|
579
656
|
if defined? Encoding
|
580
657
|
context "strings returned by #info" do
|
581
658
|
it "should default to the connection's encoding if Encoding.default_internal is nil" do
|
582
|
-
|
583
|
-
|
659
|
+
with_internal_encoding nil do
|
660
|
+
@client.info[:version].encoding.should eql(Encoding.find('utf-8'))
|
584
661
|
|
585
|
-
|
586
|
-
|
662
|
+
client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii'))
|
663
|
+
client2.info[:version].encoding.should eql(Encoding.find('us-ascii'))
|
664
|
+
end
|
587
665
|
end
|
588
666
|
|
589
667
|
it "should use Encoding.default_internal" do
|
590
|
-
|
591
|
-
|
592
|
-
|
593
|
-
|
668
|
+
with_internal_encoding 'utf-8' do
|
669
|
+
@client.info[:version].encoding.should eql(Encoding.default_internal)
|
670
|
+
end
|
671
|
+
|
672
|
+
with_internal_encoding 'us-ascii' do
|
673
|
+
@client.info[:version].encoding.should eql(Encoding.default_internal)
|
674
|
+
end
|
594
675
|
end
|
595
676
|
end
|
596
677
|
end
|
@@ -618,18 +699,22 @@ describe Mysql2::Client do
|
|
618
699
|
if defined? Encoding
|
619
700
|
context "strings returned by #server_info" do
|
620
701
|
it "should default to the connection's encoding if Encoding.default_internal is nil" do
|
621
|
-
|
622
|
-
|
702
|
+
with_internal_encoding nil do
|
703
|
+
@client.server_info[:version].encoding.should eql(Encoding.find('utf-8'))
|
623
704
|
|
624
|
-
|
625
|
-
|
705
|
+
client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii'))
|
706
|
+
client2.server_info[:version].encoding.should eql(Encoding.find('us-ascii'))
|
707
|
+
end
|
626
708
|
end
|
627
709
|
|
628
710
|
it "should use Encoding.default_internal" do
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
711
|
+
with_internal_encoding 'utf-8' do
|
712
|
+
@client.server_info[:version].encoding.should eql(Encoding.default_internal)
|
713
|
+
end
|
714
|
+
|
715
|
+
with_internal_encoding 'us-ascii' do
|
716
|
+
@client.server_info[:version].encoding.should eql(Encoding.default_internal)
|
717
|
+
end
|
633
718
|
end
|
634
719
|
end
|
635
720
|
end
|
@@ -647,7 +732,7 @@ describe Mysql2::Client do
|
|
647
732
|
context 'write operations api' do
|
648
733
|
before(:each) do
|
649
734
|
@client.query "USE test"
|
650
|
-
@client.query "CREATE TABLE IF NOT EXISTS lastIdTest (`id`
|
735
|
+
@client.query "CREATE TABLE IF NOT EXISTS lastIdTest (`id` BIGINT NOT NULL AUTO_INCREMENT, blah INT(11), PRIMARY KEY (`id`))"
|
651
736
|
end
|
652
737
|
|
653
738
|
after(:each) do
|
@@ -674,6 +759,15 @@ describe Mysql2::Client do
|
|
674
759
|
@client.query "UPDATE lastIdTest SET blah=4321 WHERE id=1"
|
675
760
|
@client.affected_rows.should eql(1)
|
676
761
|
end
|
762
|
+
|
763
|
+
it "#last_id should handle BIGINT auto-increment ids above 32 bits" do
|
764
|
+
# The id column type must be BIGINT. Surprise: INT(x) is limited to 32-bits for all values of x.
|
765
|
+
# Insert a row with a given ID, this should raise the auto-increment state
|
766
|
+
@client.query "INSERT INTO lastIdTest (id, blah) VALUES (5000000000, 5000)"
|
767
|
+
@client.last_id.should eql(5000000000)
|
768
|
+
@client.query "INSERT INTO lastIdTest (blah) VALUES (5001)"
|
769
|
+
@client.last_id.should eql(5000000001)
|
770
|
+
end
|
677
771
|
end
|
678
772
|
|
679
773
|
it "should respond to #thread_id" do
|
data/spec/mysql2/error_spec.rb
CHANGED
@@ -1,72 +1,82 @@
|
|
1
1
|
# encoding: UTF-8
|
2
|
+
|
2
3
|
require 'spec_helper'
|
3
4
|
|
4
5
|
describe Mysql2::Error do
|
5
|
-
|
6
|
-
begin
|
7
|
-
@err_client = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => "utf8"))
|
8
|
-
@err_client.query("HAHAHA")
|
9
|
-
rescue Mysql2::Error => e
|
10
|
-
@error = e
|
11
|
-
ensure
|
12
|
-
@err_client.close
|
13
|
-
end
|
6
|
+
let(:client) { Mysql2::Client.new(DatabaseCredentials['root']) }
|
14
7
|
|
8
|
+
let :error do
|
15
9
|
begin
|
16
|
-
|
17
|
-
@err_client2.query("HAHAHA")
|
10
|
+
client.query("HAHAHA")
|
18
11
|
rescue Mysql2::Error => e
|
19
|
-
|
12
|
+
error = e
|
20
13
|
ensure
|
21
|
-
|
14
|
+
client.close
|
22
15
|
end
|
23
|
-
end
|
24
16
|
|
25
|
-
|
26
|
-
@error.should respond_to(:error_number)
|
17
|
+
error
|
27
18
|
end
|
28
19
|
|
29
|
-
it "
|
30
|
-
|
31
|
-
|
20
|
+
it "responds to error_number and sql_state, with aliases" do
|
21
|
+
error.should respond_to(:error_number)
|
22
|
+
error.should respond_to(:sql_state)
|
32
23
|
|
33
|
-
|
34
|
-
|
35
|
-
|
24
|
+
# Mysql gem compatibility
|
25
|
+
error.should respond_to(:errno)
|
26
|
+
error.should respond_to(:error)
|
36
27
|
end
|
37
28
|
|
38
|
-
|
39
|
-
|
40
|
-
|
29
|
+
if "".respond_to? :encoding
|
30
|
+
let :error do
|
31
|
+
client = Mysql2::Client.new(DatabaseCredentials['root'])
|
32
|
+
begin
|
33
|
+
client.query("\xE9\x80\xA0\xE5\xAD\x97")
|
34
|
+
rescue Mysql2::Error => e
|
35
|
+
error = e
|
36
|
+
ensure
|
37
|
+
client.close
|
38
|
+
end
|
41
39
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
40
|
+
error
|
41
|
+
end
|
42
|
+
|
43
|
+
let :bad_err do
|
44
|
+
client = Mysql2::Client.new(DatabaseCredentials['root'])
|
45
|
+
begin
|
46
|
+
client.query("\xE5\xC6\x7D\x1F")
|
47
|
+
rescue Mysql2::Error => e
|
48
|
+
error = e
|
49
|
+
ensure
|
50
|
+
client.close
|
50
51
|
end
|
52
|
+
|
53
|
+
error
|
51
54
|
end
|
52
55
|
|
53
|
-
it "
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
56
|
+
it "returns error messages as UTF-8 by default" do
|
57
|
+
with_internal_encoding nil do
|
58
|
+
error.message.encoding.should eql(Encoding::UTF_8)
|
59
|
+
error.message.valid_encoding?
|
60
|
+
|
61
|
+
bad_err.message.encoding.should eql(Encoding::UTF_8)
|
62
|
+
bad_err.message.valid_encoding?
|
63
|
+
|
64
|
+
bad_err.message.should include("??}\u001F")
|
60
65
|
end
|
61
66
|
end
|
62
67
|
|
63
|
-
it "
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
68
|
+
it "returns sql state as ASCII" do
|
69
|
+
error.sql_state.encoding.should eql(Encoding::US_ASCII)
|
70
|
+
error.sql_state.valid_encoding?
|
71
|
+
end
|
72
|
+
|
73
|
+
it "returns error messages and sql state in Encoding.default_internal if set" do
|
74
|
+
with_internal_encoding 'UTF-16LE' do
|
75
|
+
error.message.encoding.should eql(Encoding.default_internal)
|
76
|
+
error.message.valid_encoding?
|
77
|
+
|
78
|
+
bad_err.message.encoding.should eql(Encoding.default_internal)
|
79
|
+
bad_err.message.valid_encoding?
|
70
80
|
end
|
71
81
|
end
|
72
82
|
end
|