mysql2 0.2.21 → 0.2.22

Sign up to get free protection for your applications and to get access to all the features.
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
- attr_accessor :error_number, :sql_state
5
+ REPLACEMENT_CHAR = '?'
6
+ ENCODE_OPTS = {:undef => :replace, :invalid => :replace, :replace => REPLACEMENT_CHAR}
4
7
 
5
- def initialize msg
6
- super
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
@@ -1,3 +1,3 @@
1
1
  module Mysql2
2
- VERSION = "0.2.21"
2
+ VERSION = "0.2.22"
3
3
  end
@@ -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
- @client = Mysql2::Client.new(:default_file => cnf_file, :default_group => "test")
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
- 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.")
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
- Encoding.default_internal = nil
583
- @client.info[:version].encoding.should eql(Encoding.find('utf-8'))
659
+ with_internal_encoding nil do
660
+ @client.info[:version].encoding.should eql(Encoding.find('utf-8'))
584
661
 
585
- client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii'))
586
- client2.info[:version].encoding.should eql(Encoding.find('us-ascii'))
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
- Encoding.default_internal = Encoding.find('utf-8')
591
- @client.info[:version].encoding.should eql(Encoding.default_internal)
592
- Encoding.default_internal = Encoding.find('us-ascii')
593
- @client.info[:version].encoding.should eql(Encoding.default_internal)
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
- Encoding.default_internal = nil
622
- @client.server_info[:version].encoding.should eql(Encoding.find('utf-8'))
702
+ with_internal_encoding nil do
703
+ @client.server_info[:version].encoding.should eql(Encoding.find('utf-8'))
623
704
 
624
- client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => 'ascii'))
625
- client2.server_info[:version].encoding.should eql(Encoding.find('us-ascii'))
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
- Encoding.default_internal = Encoding.find('utf-8')
630
- @client.server_info[:version].encoding.should eql(Encoding.default_internal)
631
- Encoding.default_internal = Encoding.find('us-ascii')
632
- @client.server_info[:version].encoding.should eql(Encoding.default_internal)
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` int(11) NOT NULL AUTO_INCREMENT, blah INT(11), PRIMARY KEY (`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
@@ -1,72 +1,82 @@
1
1
  # encoding: UTF-8
2
+
2
3
  require 'spec_helper'
3
4
 
4
5
  describe Mysql2::Error do
5
- before(:each) do
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
- @err_client2 = Mysql2::Client.new(DatabaseCredentials['root'].merge(:encoding => "big5"))
17
- @err_client2.query("HAHAHA")
10
+ client.query("HAHAHA")
18
11
  rescue Mysql2::Error => e
19
- @error2 = e
12
+ error = e
20
13
  ensure
21
- @err_client2.close
14
+ client.close
22
15
  end
23
- end
24
16
 
25
- it "should respond to #error_number" do
26
- @error.should respond_to(:error_number)
17
+ error
27
18
  end
28
19
 
29
- it "should respond to #sql_state" do
30
- @error.should respond_to(:sql_state)
31
- end
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
- # Mysql gem compatibility
34
- it "should alias #error_number to #errno" do
35
- @error.should respond_to(:errno)
24
+ # Mysql gem compatibility
25
+ error.should respond_to(:errno)
26
+ error.should respond_to(:error)
36
27
  end
37
28
 
38
- it "should alias #message to #error" do
39
- @error.should respond_to(:error)
40
- end
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
- unless RUBY_VERSION =~ /1.8/
43
- it "#message encoding should match the connection's encoding, or Encoding.default_internal if set" do
44
- if Encoding.default_internal.nil?
45
- @error.message.encoding.should eql(@err_client.encoding)
46
- @error2.message.encoding.should eql(@err_client2.encoding)
47
- else
48
- @error.message.encoding.should eql(Encoding.default_internal)
49
- @error2.message.encoding.should eql(Encoding.default_internal)
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 "#error encoding should match the connection's encoding, or Encoding.default_internal if set" do
54
- if Encoding.default_internal.nil?
55
- @error.error.encoding.should eql(@err_client.encoding)
56
- @error2.error.encoding.should eql(@err_client2.encoding)
57
- else
58
- @error.error.encoding.should eql(Encoding.default_internal)
59
- @error2.error.encoding.should eql(Encoding.default_internal)
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 "#sql_state encoding should match the connection's encoding, or Encoding.default_internal if set" do
64
- if Encoding.default_internal.nil?
65
- @error.sql_state.encoding.should eql(@err_client.encoding)
66
- @error2.sql_state.encoding.should eql(@err_client2.encoding)
67
- else
68
- @error.sql_state.encoding.should eql(Encoding.default_internal)
69
- @error2.sql_state.encoding.should eql(Encoding.default_internal)
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