mysql2 0.3.18 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1 -0
  3. data/LICENSE +21 -0
  4. data/README.md +69 -17
  5. data/examples/eventmachine.rb +1 -1
  6. data/examples/threaded.rb +4 -6
  7. data/ext/mysql2/client.c +230 -179
  8. data/ext/mysql2/client.h +18 -1
  9. data/ext/mysql2/extconf.rb +95 -35
  10. data/ext/mysql2/infile.c +2 -2
  11. data/ext/mysql2/mysql2_ext.c +1 -0
  12. data/ext/mysql2/mysql2_ext.h +5 -6
  13. data/ext/mysql2/mysql_enc_name_to_ruby.h +2 -2
  14. data/ext/mysql2/mysql_enc_to_ruby.h +25 -22
  15. data/ext/mysql2/result.c +509 -138
  16. data/ext/mysql2/result.h +12 -6
  17. data/ext/mysql2/statement.c +504 -0
  18. data/ext/mysql2/statement.h +19 -0
  19. data/lib/mysql2/client.rb +71 -25
  20. data/lib/mysql2/console.rb +1 -1
  21. data/lib/mysql2/em.rb +5 -6
  22. data/lib/mysql2/error.rb +18 -27
  23. data/lib/mysql2/field.rb +3 -0
  24. data/lib/mysql2/statement.rb +17 -0
  25. data/lib/mysql2/version.rb +1 -1
  26. data/lib/mysql2.rb +38 -18
  27. data/spec/em/em_spec.rb +21 -21
  28. data/spec/mysql2/client_spec.rb +456 -362
  29. data/spec/mysql2/error_spec.rb +37 -36
  30. data/spec/mysql2/result_spec.rb +222 -208
  31. data/spec/mysql2/statement_spec.rb +703 -0
  32. data/spec/spec_helper.rb +7 -0
  33. data/spec/ssl/ca-cert.pem +17 -0
  34. data/spec/ssl/ca-key.pem +27 -0
  35. data/spec/ssl/ca.cnf +22 -0
  36. data/spec/ssl/cert.cnf +22 -0
  37. data/spec/ssl/client-cert.pem +17 -0
  38. data/spec/ssl/client-key.pem +27 -0
  39. data/spec/ssl/client-req.pem +15 -0
  40. data/spec/ssl/gen_certs.sh +48 -0
  41. data/spec/ssl/pkcs8-client-key.pem +28 -0
  42. data/spec/ssl/pkcs8-server-key.pem +28 -0
  43. data/spec/ssl/server-cert.pem +17 -0
  44. data/spec/ssl/server-key.pem +27 -0
  45. data/spec/ssl/server-req.pem +15 -0
  46. data/support/mysql_enc_to_ruby.rb +7 -8
  47. data/support/ruby_enc_to_mysql.rb +1 -1
  48. metadata +42 -47
data/lib/mysql2/client.rb CHANGED
@@ -1,24 +1,27 @@
1
1
  module Mysql2
2
2
  class Client
3
3
  attr_reader :query_options, :read_timeout
4
- @@default_query_options = {
5
- :as => :hash, # the type of object you want each row back as; also supports :array (an array of values)
6
- :async => false, # don't wait for a result after sending the query, you'll have to monitor the socket yourself then eventually call Mysql2::Client#async_result
7
- :cast_booleans => false, # cast tinyint(1) fields as true/false in ruby
8
- :symbolize_keys => false, # return field names as symbols instead of strings
9
- :database_timezone => :local, # timezone Mysql2 will assume datetime objects are stored in
10
- :application_timezone => nil, # timezone Mysql2 will convert to before handing the object back to the caller
11
- :cache_rows => true, # tells Mysql2 to use it's internal row cache for results
12
- :connect_flags => REMEMBER_OPTIONS | LONG_PASSWORD | LONG_FLAG | TRANSACTIONS | PROTOCOL_41 | SECURE_CONNECTION,
13
- :cast => true,
14
- :default_file => nil,
15
- :default_group => nil
16
- }
4
+
5
+ def self.default_query_options
6
+ @default_query_options ||= {
7
+ :as => :hash, # the type of object you want each row back as; also supports :array (an array of values)
8
+ :async => false, # don't wait for a result after sending the query, you'll have to monitor the socket yourself then eventually call Mysql2::Client#async_result
9
+ :cast_booleans => false, # cast tinyint(1) fields as true/false in ruby
10
+ :symbolize_keys => false, # return field names as symbols instead of strings
11
+ :database_timezone => :local, # timezone Mysql2 will assume datetime objects are stored in
12
+ :application_timezone => nil, # timezone Mysql2 will convert to before handing the object back to the caller
13
+ :cache_rows => true, # tells Mysql2 to use its internal row cache for results
14
+ :connect_flags => REMEMBER_OPTIONS | LONG_PASSWORD | LONG_FLAG | TRANSACTIONS | PROTOCOL_41 | SECURE_CONNECTION,
15
+ :cast => true,
16
+ :default_file => nil,
17
+ :default_group => nil,
18
+ }
19
+ end
17
20
 
18
21
  def initialize(opts = {})
19
- opts = Mysql2::Util.key_hash_as_symbols( opts )
22
+ opts = Mysql2::Util.key_hash_as_symbols(opts)
20
23
  @read_timeout = nil
21
- @query_options = @@default_query_options.dup
24
+ @query_options = self.class.default_query_options.dup
22
25
  @query_options.merge! opts
23
26
 
24
27
  initialize_ext
@@ -26,13 +29,14 @@ module Mysql2
26
29
  # Set default connect_timeout to avoid unlimited retries from signal interruption
27
30
  opts[:connect_timeout] = 120 unless opts.key?(:connect_timeout)
28
31
 
29
- [:reconnect, :connect_timeout, :local_infile, :read_timeout, :write_timeout, :default_file, :default_group, :secure_auth, :init_command].each do |key|
32
+ # TODO: stricter validation rather than silent massaging
33
+ [:reconnect, :connect_timeout, :local_infile, :read_timeout, :write_timeout, :default_file, :default_group, :secure_auth, :init_command, :automatic_close].each do |key|
30
34
  next unless opts.key?(key)
31
35
  case key
32
- when :reconnect, :local_infile, :secure_auth
33
- send(:"#{key}=", !!opts[key])
36
+ when :reconnect, :local_infile, :secure_auth, :automatic_close
37
+ send(:"#{key}=", !!opts[key]) # rubocop:disable Style/DoubleNegation
34
38
  when :connect_timeout, :read_timeout, :write_timeout
35
- send(:"#{key}=", opts[key].to_i)
39
+ send(:"#{key}=", opts[key]) unless opts[key].nil?
36
40
  else
37
41
  send(:"#{key}=", opts[key])
38
42
  end
@@ -44,7 +48,21 @@ module Mysql2
44
48
  ssl_options = opts.values_at(:sslkey, :sslcert, :sslca, :sslcapath, :sslcipher)
45
49
  ssl_set(*ssl_options) if ssl_options.any?
46
50
 
47
- if [:user,:pass,:hostname,:dbname,:db,:sock].any?{|k| @query_options.has_key?(k) }
51
+ case opts[:flags]
52
+ when Array
53
+ flags = parse_flags_array(opts[:flags], @query_options[:connect_flags])
54
+ when String
55
+ flags = parse_flags_array(opts[:flags].split(' '), @query_options[:connect_flags])
56
+ when Integer
57
+ flags = @query_options[:connect_flags] | opts[:flags]
58
+ else
59
+ flags = @query_options[:connect_flags]
60
+ end
61
+
62
+ # SSL verify is a connection flag rather than a mysql_ssl_set option
63
+ flags |= SSL_VERIFY_SERVER_CERT if opts[:sslverify] && ssl_options.any?
64
+
65
+ if [:user, :pass, :hostname, :dbname, :db, :sock].any? { |k| @query_options.key?(k) }
48
66
  warn "============= WARNING FROM mysql2 ============="
49
67
  warn "The options :user, :pass, :hostname, :dbname, :db, and :sock will be deprecated at some point in the future."
50
68
  warn "Instead, please use :username, :password, :host, :port, :database, :socket, :flags for the options."
@@ -57,7 +75,6 @@ module Mysql2
57
75
  port = opts[:port]
58
76
  database = opts[:database] || opts[:dbname] || opts[:db]
59
77
  socket = opts[:socket] || opts[:sock]
60
- flags = opts[:flags] ? opts[:flags] | @query_options[:connect_flags] : @query_options[:connect_flags]
61
78
 
62
79
  # Correct the data types before passing these values down to the C level
63
80
  user = user.to_s unless user.nil?
@@ -70,8 +87,30 @@ module Mysql2
70
87
  connect user, pass, host, port, database, socket, flags
71
88
  end
72
89
 
73
- def self.default_query_options
74
- @@default_query_options
90
+ def parse_flags_array(flags, initial = 0)
91
+ flags.reduce(initial) do |memo, f|
92
+ fneg = f.start_with?('-') ? f[1..-1] : nil
93
+ if fneg && fneg =~ /^\w+$/ && Mysql2::Client.const_defined?(fneg)
94
+ memo & ~ Mysql2::Client.const_get(fneg)
95
+ elsif f && f =~ /^\w+$/ && Mysql2::Client.const_defined?(f)
96
+ memo | Mysql2::Client.const_get(f)
97
+ else
98
+ warn "Unknown MySQL connection flag: '#{f}'"
99
+ memo
100
+ end
101
+ end
102
+ end
103
+
104
+ if Thread.respond_to?(:handle_interrupt)
105
+ def query(sql, options = {})
106
+ Thread.handle_interrupt(::Mysql2::Util::TimeoutError => :never) do
107
+ _query(sql, @query_options.merge(options))
108
+ end
109
+ end
110
+ else
111
+ def query(sql, options = {})
112
+ _query(sql, @query_options.merge(options))
113
+ end
75
114
  end
76
115
 
77
116
  def query_info
@@ -82,9 +121,16 @@ module Mysql2
82
121
  info_hash
83
122
  end
84
123
 
85
- private
86
- def self.local_offset
124
+ def info
125
+ self.class.info
126
+ end
127
+
128
+ class << self
129
+ private
130
+
131
+ def local_offset
87
132
  ::Time.local(2010).utc_offset.to_r / 86400
88
133
  end
134
+ end
89
135
  end
90
136
  end
@@ -1,5 +1,5 @@
1
1
  # Loaded by script/console. Land helpers here.
2
2
 
3
- Pry.config.prompt = lambda do |context, nesting, pry|
3
+ Pry.config.prompt = lambda do |context, *|
4
4
  "[mysql2] #{context}> "
5
5
  end
data/lib/mysql2/em.rb CHANGED
@@ -17,7 +17,7 @@ module Mysql2
17
17
  detach
18
18
  begin
19
19
  result = @client.async_result
20
- rescue Exception => e
20
+ rescue => e
21
21
  @deferable.fail(e)
22
22
  else
23
23
  @deferable.succeed(result)
@@ -34,17 +34,16 @@ module Mysql2
34
34
  end
35
35
 
36
36
  def close(*args)
37
- if @watch
38
- @watch.detach if @watch.watching?
39
- end
37
+ @watch.detach if @watch && @watch.watching?
38
+
40
39
  super(*args)
41
40
  end
42
41
 
43
- def query(sql, opts={})
42
+ def query(sql, opts = {})
44
43
  if ::EM.reactor_running?
45
44
  super(sql, opts.merge(:async => true))
46
45
  deferable = ::EM::DefaultDeferrable.new
47
- @watch = ::EM.watch(self.socket, Watcher, self, deferable)
46
+ @watch = ::EM.watch(socket, Watcher, self, deferable)
48
47
  @watch.notify_readable = true
49
48
  deferable
50
49
  else
data/lib/mysql2/error.rb CHANGED
@@ -2,25 +2,31 @@
2
2
 
3
3
  module Mysql2
4
4
  class Error < StandardError
5
- REPLACEMENT_CHAR = '?'
6
- ENCODE_OPTS = {:undef => :replace, :invalid => :replace, :replace => REPLACEMENT_CHAR}
5
+ ENCODE_OPTS = {
6
+ :undef => :replace,
7
+ :invalid => :replace,
8
+ :replace => '?'.freeze,
9
+ }.freeze
7
10
 
8
- attr_accessor :error_number
9
- attr_reader :sql_state
10
- attr_writer :server_version
11
+ attr_reader :error_number, :sql_state
11
12
 
12
13
  # Mysql gem compatibility
13
14
  alias_method :errno, :error_number
14
15
  alias_method :error, :message
15
16
 
16
- def initialize(msg, server_version=nil)
17
- self.server_version = server_version
17
+ def initialize(msg)
18
+ @server_version ||= nil
18
19
 
19
20
  super(clean_message(msg))
20
21
  end
21
22
 
22
- def sql_state=(state)
23
- @sql_state = ''.respond_to?(:encode) ? state.encode(ENCODE_OPTS) : state
23
+ def self.new_with_args(msg, server_version, error_number, sql_state)
24
+ err = allocate
25
+ err.instance_variable_set('@server_version', server_version)
26
+ err.instance_variable_set('@error_number', error_number)
27
+ err.instance_variable_set('@sql_state', sql_state.respond_to?(:encode) ? sql_state.encode(ENCODE_OPTS) : sql_state)
28
+ err.send(:initialize, msg)
29
+ err
24
30
  end
25
31
 
26
32
  private
@@ -30,7 +36,7 @@ module Mysql2
30
36
  # variable.
31
37
  #
32
38
  # See http://dev.mysql.com/doc/refman/5.5/en/charset-errors.html for
33
- # more contetx.
39
+ # more context.
34
40
  #
35
41
  # Before MySQL 5.5 error message template strings are in whatever encoding
36
42
  # is associated with the error message language.
@@ -53,27 +59,12 @@ module Mysql2
53
59
  #
54
60
  # Returns a valid UTF-8 string in Ruby 1.9+, the original string on Ruby 1.8
55
61
  def clean_message(message)
56
- return message if !message.respond_to?(:encoding)
62
+ return message unless message.respond_to?(:encode)
57
63
 
58
64
  if @server_version && @server_version > 50500
59
65
  message.encode(ENCODE_OPTS)
60
66
  else
61
- if message.respond_to? :scrub
62
- message.scrub(REPLACEMENT_CHAR).encode(ENCODE_OPTS)
63
- else
64
- # This is ugly as hell but Ruby 1.9 doesn't provide a way to clean a string
65
- # and retain it's valid UTF-8 characters, that I know of.
66
-
67
- new_message = "".force_encoding(Encoding::UTF_8)
68
- message.chars.each do |char|
69
- if char.valid_encoding?
70
- new_message << char
71
- else
72
- new_message << REPLACEMENT_CHAR
73
- end
74
- end
75
- new_message.encode(ENCODE_OPTS)
76
- end
67
+ message.encode(Encoding::UTF_8, ENCODE_OPTS)
77
68
  end
78
69
  end
79
70
  end
@@ -0,0 +1,3 @@
1
+ module Mysql2
2
+ Field = Struct.new(:name, :type)
3
+ end
@@ -0,0 +1,17 @@
1
+ module Mysql2
2
+ class Statement
3
+ include Enumerable
4
+
5
+ if Thread.respond_to?(:handle_interrupt)
6
+ def execute(*args)
7
+ Thread.handle_interrupt(::Mysql2::Util::TimeoutError => :never) do
8
+ _execute(*args)
9
+ end
10
+ end
11
+ else
12
+ def execute(*args)
13
+ _execute(*args)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -1,3 +1,3 @@
1
1
  module Mysql2
2
- VERSION = "0.3.18"
2
+ VERSION = "0.4.4"
3
3
  end
data/lib/mysql2.rb CHANGED
@@ -8,16 +8,16 @@ require 'rational' unless RUBY_VERSION >= '1.9.2'
8
8
  # Or to bomb out with a clear error message instead of a linker crash
9
9
  if RUBY_PLATFORM =~ /mswin|mingw/
10
10
  dll_path = if ENV['RUBY_MYSQL2_LIBMYSQL_DLL']
11
- # If this environment variable is set, it overrides any other paths
12
- # The user is advised to use backslashes not forward slashes
13
- ENV['RUBY_MYSQL2_LIBMYSQL_DLL'].dup
14
- elsif File.exist?(File.expand_path('../vendor/libmysql.dll', File.dirname(__FILE__)))
15
- # Use vendor/libmysql.dll if it exists, convert slashes for Win32 LoadLibrary
16
- File.expand_path('../vendor/libmysql.dll', File.dirname(__FILE__)).gsub('/', '\\')
17
- else
18
- # This will use default / system library paths
19
- 'libmysql.dll'
20
- end
11
+ # If this environment variable is set, it overrides any other paths
12
+ # The user is advised to use backslashes not forward slashes
13
+ ENV['RUBY_MYSQL2_LIBMYSQL_DLL']
14
+ elsif File.exist?(File.expand_path('../vendor/libmysql.dll', File.dirname(__FILE__)))
15
+ # Use vendor/libmysql.dll if it exists, convert slashes for Win32 LoadLibrary
16
+ File.expand_path('../vendor/libmysql.dll', File.dirname(__FILE__)).tr('/', '\\')
17
+ else
18
+ # This will use default / system library paths
19
+ 'libmysql.dll'
20
+ end
21
21
 
22
22
  require 'Win32API'
23
23
  LoadLibrary = Win32API.new('Kernel32', 'LoadLibrary', ['P'], 'I')
@@ -31,6 +31,8 @@ require 'mysql2/error'
31
31
  require 'mysql2/mysql2'
32
32
  require 'mysql2/result'
33
33
  require 'mysql2/client'
34
+ require 'mysql2/field'
35
+ require 'mysql2/statement'
34
36
 
35
37
  # = Mysql2
36
38
  #
@@ -51,14 +53,32 @@ if defined?(ActiveRecord::VERSION::STRING) && ActiveRecord::VERSION::STRING < "3
51
53
  end
52
54
 
53
55
  # For holding utility methods
54
- module Mysql2::Util
56
+ module Mysql2
57
+ module Util
58
+ #
59
+ # Rekey a string-keyed hash with equivalent symbols.
60
+ #
61
+ def self.key_hash_as_symbols(hash)
62
+ return nil unless hash
63
+ Hash[hash.map { |k, v| [k.to_sym, v] }]
64
+ end
55
65
 
56
- #
57
- # Rekey a string-keyed hash with equivalent symbols.
58
- #
59
- def self.key_hash_as_symbols(hash)
60
- return nil unless hash
61
- Hash[hash.map { |k,v| [k.to_sym, v] }]
66
+ #
67
+ # In Mysql2::Client#query and Mysql2::Statement#execute,
68
+ # Thread#handle_interrupt is used to prevent Timeout#timeout
69
+ # from interrupting query execution.
70
+ #
71
+ # Timeout::ExitException was removed in Ruby 2.3.0, 2.2.3, and 2.1.8,
72
+ # but is present in earlier 2.1.x and 2.2.x, so we provide a shim.
73
+ #
74
+ if Thread.respond_to?(:handle_interrupt)
75
+ require 'timeout'
76
+ # rubocop:disable Style/ConstantName
77
+ TimeoutError = if defined?(::Timeout::ExitException)
78
+ ::Timeout::ExitException
79
+ else
80
+ ::Timeout::Error
81
+ end
82
+ end
62
83
  end
63
-
64
84
  end
data/spec/em/em_spec.rb CHANGED
@@ -4,7 +4,7 @@ begin
4
4
  require 'eventmachine'
5
5
  require 'mysql2/em'
6
6
 
7
- describe Mysql2::EM::Client do
7
+ RSpec.describe Mysql2::EM::Client do
8
8
  it "should support async queries" do
9
9
  results = []
10
10
  EM.run do
@@ -24,8 +24,8 @@ begin
24
24
  end
25
25
  end
26
26
 
27
- results[0].keys.should include("second_query")
28
- results[1].keys.should include("first_query")
27
+ expect(results[0].keys).to include("second_query")
28
+ expect(results[1].keys).to include("first_query")
29
29
  end
30
30
 
31
31
  it "should support queries in callbacks" do
@@ -44,38 +44,38 @@ begin
44
44
  end
45
45
  end
46
46
 
47
- results[0].keys.should include("first_query")
48
- results[1].keys.should include("second_query")
47
+ expect(results[0].keys).to include("first_query")
48
+ expect(results[1].keys).to include("second_query")
49
49
  end
50
50
 
51
51
  it "should not swallow exceptions raised in callbacks" do
52
- lambda {
52
+ expect {
53
53
  EM.run do
54
54
  client = Mysql2::EM::Client.new DatabaseCredentials['root']
55
55
  defer = client.query "SELECT sleep(0.1) as first_query"
56
- defer.callback do |result|
56
+ defer.callback do
57
57
  client.close
58
- raise 'some error'
58
+ fail 'some error'
59
59
  end
60
- defer.errback do |err|
60
+ defer.errback do
61
61
  # This _shouldn't_ be run, but it needed to prevent the specs from
62
62
  # freezing if this test fails.
63
63
  EM.stop_event_loop
64
64
  end
65
65
  end
66
- }.should raise_error
66
+ }.to raise_error('some error')
67
67
  end
68
68
 
69
69
  context 'when an exception is raised by the client' do
70
70
  let(:client) { Mysql2::EM::Client.new DatabaseCredentials['root'] }
71
71
  let(:error) { StandardError.new('some error') }
72
- before { client.stub(:async_result).and_raise(error) }
72
+ before { allow(client).to receive(:async_result).and_raise(error) }
73
73
 
74
74
  it "should swallow exceptions raised in by the client" do
75
75
  errors = []
76
76
  EM.run do
77
77
  defer = client.query "SELECT sleep(0.1) as first_query"
78
- defer.callback do |result|
78
+ defer.callback do
79
79
  # This _shouldn't_ be run, but it is needed to prevent the specs from
80
80
  # freezing if this test fails.
81
81
  EM.stop_event_loop
@@ -85,7 +85,7 @@ begin
85
85
  EM.stop_event_loop
86
86
  end
87
87
  end
88
- errors.should == [error]
88
+ expect(errors).to eq([error])
89
89
  end
90
90
 
91
91
  it "should fail the deferrable" do
@@ -93,19 +93,19 @@ begin
93
93
  EM.run do
94
94
  defer = client.query "SELECT sleep(0.025) as first_query"
95
95
  EM.add_timer(0.1) do
96
- defer.callback do |result|
96
+ defer.callback do
97
97
  callbacks_run << :callback
98
98
  # This _shouldn't_ be run, but it is needed to prevent the specs from
99
99
  # freezing if this test fails.
100
100
  EM.stop_event_loop
101
101
  end
102
- defer.errback do |err|
102
+ defer.errback do
103
103
  callbacks_run << :errback
104
104
  EM.stop_event_loop
105
105
  end
106
106
  end
107
107
  end
108
- callbacks_run.should == [:errback]
108
+ expect(callbacks_run).to eq([:errback])
109
109
  end
110
110
  end
111
111
 
@@ -114,17 +114,17 @@ begin
114
114
  EM.run do
115
115
  client = Mysql2::EM::Client.new DatabaseCredentials['root']
116
116
  defer = client.query("select sleep(0.025)")
117
- defer.callback do |result|
117
+ defer.callback do
118
118
  callbacks_run << :callback
119
119
  end
120
- defer.errback do |err|
120
+ defer.errback do
121
121
  callbacks_run << :errback
122
122
  end
123
123
  EM.add_timer(0.1) do
124
- callbacks_run.should == [:callback]
125
- lambda {
124
+ expect(callbacks_run).to eq([:callback])
125
+ expect {
126
126
  client.close
127
- }.should_not raise_error(/invalid binding to detach/)
127
+ }.not_to raise_error
128
128
  EM.stop_event_loop
129
129
  end
130
130
  end