mysql2 0.2.21 → 0.2.22

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/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