mysql2 0.4.2 → 0.5.5

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.
Files changed (52) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +231 -86
  3. data/ext/mysql2/client.c +527 -128
  4. data/ext/mysql2/client.h +11 -52
  5. data/ext/mysql2/extconf.rb +100 -21
  6. data/ext/mysql2/mysql2_ext.c +8 -2
  7. data/ext/mysql2/mysql2_ext.h +21 -8
  8. data/ext/mysql2/mysql_enc_name_to_ruby.h +60 -56
  9. data/ext/mysql2/mysql_enc_to_ruby.h +64 -3
  10. data/ext/mysql2/result.c +333 -109
  11. data/ext/mysql2/result.h +1 -0
  12. data/ext/mysql2/statement.c +247 -90
  13. data/ext/mysql2/statement.h +0 -2
  14. data/ext/mysql2/wait_for_single_fd.h +2 -1
  15. data/lib/mysql2/client.rb +71 -31
  16. data/lib/mysql2/em.rb +2 -4
  17. data/lib/mysql2/error.rb +52 -22
  18. data/lib/mysql2/result.rb +2 -0
  19. data/lib/mysql2/statement.rb +3 -11
  20. data/lib/mysql2/version.rb +1 -1
  21. data/lib/mysql2.rb +19 -15
  22. data/support/3A79BD29.asc +49 -0
  23. data/support/5072E1F5.asc +432 -0
  24. data/support/C74CD1D8.asc +104 -0
  25. data/support/mysql_enc_to_ruby.rb +8 -3
  26. data/support/ruby_enc_to_mysql.rb +7 -5
  27. metadata +19 -61
  28. data/examples/eventmachine.rb +0 -21
  29. data/examples/threaded.rb +0 -18
  30. data/spec/configuration.yml.example +0 -17
  31. data/spec/em/em_spec.rb +0 -135
  32. data/spec/my.cnf.example +0 -9
  33. data/spec/mysql2/client_spec.rb +0 -939
  34. data/spec/mysql2/error_spec.rb +0 -84
  35. data/spec/mysql2/result_spec.rb +0 -510
  36. data/spec/mysql2/statement_spec.rb +0 -684
  37. data/spec/rcov.opts +0 -3
  38. data/spec/spec_helper.rb +0 -94
  39. data/spec/ssl/ca-cert.pem +0 -17
  40. data/spec/ssl/ca-key.pem +0 -27
  41. data/spec/ssl/ca.cnf +0 -22
  42. data/spec/ssl/cert.cnf +0 -22
  43. data/spec/ssl/client-cert.pem +0 -17
  44. data/spec/ssl/client-key.pem +0 -27
  45. data/spec/ssl/client-req.pem +0 -15
  46. data/spec/ssl/gen_certs.sh +0 -48
  47. data/spec/ssl/pkcs8-client-key.pem +0 -28
  48. data/spec/ssl/pkcs8-server-key.pem +0 -28
  49. data/spec/ssl/server-cert.pem +0 -17
  50. data/spec/ssl/server-key.pem +0 -27
  51. data/spec/ssl/server-req.pem +0 -15
  52. data/spec/test_data +0 -1
data/lib/mysql2/client.rb CHANGED
@@ -4,21 +4,23 @@ module Mysql2
4
4
 
5
5
  def self.default_query_options
6
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,
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 | CONNECT_ATTRS,
15
+ cast: true,
16
+ default_file: nil,
17
+ default_group: nil,
18
18
  }
19
19
  end
20
20
 
21
21
  def initialize(opts = {})
22
+ raise Mysql2::Error, "Options parameter must be a Hash" unless opts.is_a? Hash
23
+
22
24
  opts = Mysql2::Util.key_hash_as_symbols(opts)
23
25
  @read_timeout = nil
24
26
  @query_options = self.class.default_query_options.dup
@@ -30,13 +32,14 @@ module Mysql2
30
32
  opts[:connect_timeout] = 120 unless opts.key?(:connect_timeout)
31
33
 
32
34
  # 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].each do |key|
35
+ %i[reconnect connect_timeout local_infile read_timeout write_timeout default_file default_group secure_auth init_command automatic_close enable_cleartext_plugin default_auth].each do |key|
34
36
  next unless opts.key?(key)
37
+
35
38
  case key
36
- when :reconnect, :local_infile, :secure_auth
39
+ when :reconnect, :local_infile, :secure_auth, :automatic_close, :enable_cleartext_plugin
37
40
  send(:"#{key}=", !!opts[key]) # rubocop:disable Style/DoubleNegation
38
41
  when :connect_timeout, :read_timeout, :write_timeout
39
- send(:"#{key}=", opts[key].to_i)
42
+ send(:"#{key}=", Integer(opts[key])) unless opts[key].nil?
40
43
  else
41
44
  send(:"#{key}=", opts[key])
42
45
  end
@@ -45,26 +48,32 @@ module Mysql2
45
48
  # force the encoding to utf8
46
49
  self.charset_name = opts[:encoding] || 'utf8'
47
50
 
51
+ mode = parse_ssl_mode(opts[:ssl_mode]) if opts[:ssl_mode]
52
+ if (mode == SSL_MODE_VERIFY_CA || mode == SSL_MODE_VERIFY_IDENTITY) && !opts[:sslca]
53
+ opts[:sslca] = find_default_ca_path
54
+ end
55
+
48
56
  ssl_options = opts.values_at(:sslkey, :sslcert, :sslca, :sslcapath, :sslcipher)
49
- ssl_set(*ssl_options) if ssl_options.any?
57
+ ssl_set(*ssl_options) if ssl_options.any? || opts.key?(:sslverify)
58
+ self.ssl_mode = mode if mode
50
59
 
51
- case opts[:flags]
60
+ flags = case opts[:flags]
52
61
  when Array
53
- flags = parse_flags_array(opts[:flags], @query_options[:connect_flags])
62
+ parse_flags_array(opts[:flags], @query_options[:connect_flags])
54
63
  when String
55
- flags = parse_flags_array(opts[:flags].split(' '), @query_options[:connect_flags])
64
+ parse_flags_array(opts[:flags].split(' '), @query_options[:connect_flags])
56
65
  when Integer
57
- flags = @query_options[:connect_flags] | opts[:flags]
66
+ @query_options[:connect_flags] | opts[:flags]
58
67
  else
59
- flags = @query_options[:connect_flags]
68
+ @query_options[:connect_flags]
60
69
  end
61
70
 
62
71
  # 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?
72
+ flags |= SSL_VERIFY_SERVER_CERT if opts[:sslverify]
64
73
 
65
- if [:user, :pass, :hostname, :dbname, :db, :sock].any? { |k| @query_options.key?(k) }
74
+ if %i[user pass hostname dbname db sock].any? { |k| @query_options.key?(k) }
66
75
  warn "============= WARNING FROM mysql2 ============="
67
- warn "The options :user, :pass, :hostname, :dbname, :db, and :sock will be deprecated at some point in the future."
76
+ warn "The options :user, :pass, :hostname, :dbname, :db, and :sock are deprecated and will be removed at some point in the future."
68
77
  warn "Instead, please use :username, :password, :host, :port, :database, :socket, :flags for the options."
69
78
  warn "============= END WARNING FROM mysql2 ========="
70
79
  end
@@ -83,8 +92,20 @@ module Mysql2
83
92
  port = port.to_i unless port.nil?
84
93
  database = database.to_s unless database.nil?
85
94
  socket = socket.to_s unless socket.nil?
95
+ conn_attrs = parse_connect_attrs(opts[:connect_attrs])
86
96
 
87
- connect user, pass, host, port, database, socket, flags
97
+ connect user, pass, host, port, database, socket, flags, conn_attrs
98
+ end
99
+
100
+ def parse_ssl_mode(mode)
101
+ m = mode.to_s.upcase
102
+ if m.start_with?('SSL_MODE_')
103
+ return Mysql2::Client.const_get(m) if Mysql2::Client.const_defined?(m)
104
+ else
105
+ x = 'SSL_MODE_' + m
106
+ return Mysql2::Client.const_get(x) if Mysql2::Client.const_defined?(x)
107
+ end
108
+ warn "Unknown MySQL ssl_mode flag: #{mode}"
88
109
  end
89
110
 
90
111
  def parse_flags_array(flags, initial = 0)
@@ -101,14 +122,32 @@ module Mysql2
101
122
  end
102
123
  end
103
124
 
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
125
+ # Find any default system CA paths to handle system roots
126
+ # by default if stricter validation is requested and no
127
+ # path is provide.
128
+ def find_default_ca_path
129
+ [
130
+ "/etc/ssl/certs/ca-certificates.crt",
131
+ "/etc/pki/tls/certs/ca-bundle.crt",
132
+ "/etc/ssl/ca-bundle.pem",
133
+ "/etc/ssl/cert.pem",
134
+ ].find { |f| File.exist?(f) }
135
+ end
136
+
137
+ # Set default program_name in performance_schema.session_connect_attrs
138
+ # and performance_schema.session_account_connect_attrs
139
+ def parse_connect_attrs(conn_attrs)
140
+ return {} if Mysql2::Client::CONNECT_ATTRS.zero?
141
+
142
+ conn_attrs ||= {}
143
+ conn_attrs[:program_name] ||= $PROGRAM_NAME
144
+ conn_attrs.each_with_object({}) do |(key, value), hash|
145
+ hash[key.to_s] = value.to_s
109
146
  end
110
- else
111
- def query(sql, options = {})
147
+ end
148
+
149
+ def query(sql, options = {})
150
+ Thread.handle_interrupt(::Mysql2::Util::TIMEOUT_ERROR_NEVER) do
112
151
  _query(sql, @query_options.merge(options))
113
152
  end
114
153
  end
@@ -116,6 +155,7 @@ module Mysql2
116
155
  def query_info
117
156
  info = query_info_string
118
157
  return {} unless info
158
+
119
159
  info_hash = {}
120
160
  info.split.each_slice(2) { |s| info_hash[s[0].downcase.delete(':').to_sym] = s[1].to_i }
121
161
  info_hash
data/lib/mysql2/em.rb CHANGED
@@ -1,5 +1,3 @@
1
- # encoding: utf-8
2
-
3
1
  require 'eventmachine'
4
2
  require 'mysql2'
5
3
 
@@ -17,7 +15,7 @@ module Mysql2
17
15
  detach
18
16
  begin
19
17
  result = @client.async_result
20
- rescue => e
18
+ rescue StandardError => e
21
19
  @deferable.fail(e)
22
20
  else
23
21
  @deferable.succeed(result)
@@ -41,7 +39,7 @@ module Mysql2
41
39
 
42
40
  def query(sql, opts = {})
43
41
  if ::EM.reactor_running?
44
- super(sql, opts.merge(:async => true))
42
+ super(sql, opts.merge(async: true))
45
43
  deferable = ::EM::DefaultDeferrable.new
46
44
  @watch = ::EM.watch(socket, Watcher, self, deferable)
47
45
  @watch.notify_readable = true
data/lib/mysql2/error.rb CHANGED
@@ -1,32 +1,66 @@
1
- # encoding: UTF-8
2
-
3
1
  module Mysql2
4
2
  class Error < StandardError
5
3
  ENCODE_OPTS = {
6
- :undef => :replace,
7
- :invalid => :replace,
8
- :replace => '?'.freeze,
4
+ undef: :replace,
5
+ invalid: :replace,
6
+ replace: '?'.freeze,
7
+ }.freeze
8
+
9
+ ConnectionError = Class.new(Error)
10
+ TimeoutError = Class.new(Error)
11
+
12
+ CODES = {
13
+ 1205 => TimeoutError, # ER_LOCK_WAIT_TIMEOUT
14
+
15
+ 1044 => ConnectionError, # ER_DBACCESS_DENIED_ERROR
16
+ 1045 => ConnectionError, # ER_ACCESS_DENIED_ERROR
17
+ 1152 => ConnectionError, # ER_ABORTING_CONNECTION
18
+ 1153 => ConnectionError, # ER_NET_PACKET_TOO_LARGE
19
+ 1154 => ConnectionError, # ER_NET_READ_ERROR_FROM_PIPE
20
+ 1155 => ConnectionError, # ER_NET_FCNTL_ERROR
21
+ 1156 => ConnectionError, # ER_NET_PACKETS_OUT_OF_ORDER
22
+ 1157 => ConnectionError, # ER_NET_UNCOMPRESS_ERROR
23
+ 1158 => ConnectionError, # ER_NET_READ_ERROR
24
+ 1159 => ConnectionError, # ER_NET_READ_INTERRUPTED
25
+ 1160 => ConnectionError, # ER_NET_ERROR_ON_WRITE
26
+ 1161 => ConnectionError, # ER_NET_WRITE_INTERRUPTED
27
+ 1927 => ConnectionError, # ER_CONNECTION_KILLED
28
+
29
+ 2001 => ConnectionError, # CR_SOCKET_CREATE_ERROR
30
+ 2002 => ConnectionError, # CR_CONNECTION_ERROR
31
+ 2003 => ConnectionError, # CR_CONN_HOST_ERROR
32
+ 2004 => ConnectionError, # CR_IPSOCK_ERROR
33
+ 2005 => ConnectionError, # CR_UNKNOWN_HOST
34
+ 2006 => ConnectionError, # CR_SERVER_GONE_ERROR
35
+ 2007 => ConnectionError, # CR_VERSION_ERROR
36
+ 2009 => ConnectionError, # CR_WRONG_HOST_INFO
37
+ 2012 => ConnectionError, # CR_SERVER_HANDSHAKE_ERR
38
+ 2013 => ConnectionError, # CR_SERVER_LOST
39
+ 2020 => ConnectionError, # CR_NET_PACKET_TOO_LARGE
40
+ 2026 => ConnectionError, # CR_SSL_CONNECTION_ERROR
41
+ 2027 => ConnectionError, # CR_MALFORMED_PACKET
42
+ 2047 => ConnectionError, # CR_CONN_UNKNOW_PROTOCOL
43
+ 2048 => ConnectionError, # CR_INVALID_CONN_HANDLE
44
+ 2049 => ConnectionError, # CR_UNUSED_1
9
45
  }.freeze
10
46
 
11
47
  attr_reader :error_number, :sql_state
12
48
 
13
49
  # Mysql gem compatibility
14
- alias_method :errno, :error_number
15
- alias_method :error, :message
50
+ alias errno error_number
51
+ alias error message
16
52
 
17
- def initialize(msg)
18
- @server_version ||= nil
53
+ def initialize(msg, server_version = nil, error_number = nil, sql_state = nil)
54
+ @server_version = server_version
55
+ @error_number = error_number
56
+ @sql_state = sql_state ? sql_state.encode(**ENCODE_OPTS) : nil
19
57
 
20
58
  super(clean_message(msg))
21
59
  end
22
60
 
23
61
  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
62
+ error_class = CODES.fetch(error_number, self)
63
+ error_class.new(msg, server_version, error_number, sql_state)
30
64
  end
31
65
 
32
66
  private
@@ -55,16 +89,12 @@ module Mysql2
55
89
  # encoding, we'll assume UTF-8 and clean the string of anything that's not a
56
90
  # valid UTF-8 character.
57
91
  #
58
- # Except for if we're on 1.8, where we'll do nothing ;)
59
- #
60
- # Returns a valid UTF-8 string in Ruby 1.9+, the original string on Ruby 1.8
92
+ # Returns a valid UTF-8 string.
61
93
  def clean_message(message)
62
- return message unless message.respond_to?(:encode)
63
-
64
94
  if @server_version && @server_version > 50500
65
- message.encode(ENCODE_OPTS)
95
+ message.encode(**ENCODE_OPTS)
66
96
  else
67
- message.encode(Encoding::UTF_8, ENCODE_OPTS)
97
+ message.encode(Encoding::UTF_8, **ENCODE_OPTS)
68
98
  end
69
99
  end
70
100
  end
data/lib/mysql2/result.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  module Mysql2
2
2
  class Result
3
+ attr_reader :server_flags
4
+
3
5
  include Enumerable
4
6
  end
5
7
  end
@@ -1,16 +1,8 @@
1
1
  module Mysql2
2
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)
3
+ def execute(*args, **kwargs)
4
+ Thread.handle_interrupt(::Mysql2::Util::TIMEOUT_ERROR_NEVER) do
5
+ _execute(*args, **kwargs)
14
6
  end
15
7
  end
16
8
  end
@@ -1,3 +1,3 @@
1
1
  module Mysql2
2
- VERSION = "0.4.2"
2
+ VERSION = "0.5.5".freeze
3
3
  end
data/lib/mysql2.rb CHANGED
@@ -1,7 +1,5 @@
1
- # encoding: UTF-8
2
1
  require 'date'
3
2
  require 'bigdecimal'
4
- require 'rational' unless RUBY_VERSION >= '1.9.2'
5
3
 
6
4
  # Load libmysql.dll before requiring mysql2/mysql2.so
7
5
  # This gives a chance to be flexible about the load path
@@ -13,16 +11,23 @@ if RUBY_PLATFORM =~ /mswin|mingw/
13
11
  ENV['RUBY_MYSQL2_LIBMYSQL_DLL']
14
12
  elsif File.exist?(File.expand_path('../vendor/libmysql.dll', File.dirname(__FILE__)))
15
13
  # Use vendor/libmysql.dll if it exists, convert slashes for Win32 LoadLibrary
16
- File.expand_path('../vendor/libmysql.dll', File.dirname(__FILE__)).tr('/', '\\')
14
+ File.expand_path('../vendor/libmysql.dll', File.dirname(__FILE__))
15
+ elsif defined?(RubyInstaller)
16
+ # RubyInstaller-2.4+ native build doesn't need DLL preloading
17
17
  else
18
18
  # This will use default / system library paths
19
19
  'libmysql.dll'
20
20
  end
21
21
 
22
- require 'Win32API'
23
- LoadLibrary = Win32API.new('Kernel32', 'LoadLibrary', ['P'], 'I')
24
- if 0 == LoadLibrary.call(dll_path)
25
- abort "Failed to load libmysql.dll from #{dll_path}"
22
+ if dll_path
23
+ require 'fiddle'
24
+ kernel32 = Fiddle.dlopen 'kernel32'
25
+ load_library = Fiddle::Function.new(
26
+ kernel32['LoadLibraryW'], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT,
27
+ )
28
+ if load_library.call(dll_path.encode('utf-16le')).zero?
29
+ abort "Failed to load libmysql.dll from #{dll_path}"
30
+ end
26
31
  end
27
32
  end
28
33
 
@@ -60,6 +65,7 @@ module Mysql2
60
65
  #
61
66
  def self.key_hash_as_symbols(hash)
62
67
  return nil unless hash
68
+
63
69
  Hash[hash.map { |k, v| [k.to_sym, v] }]
64
70
  end
65
71
 
@@ -71,14 +77,12 @@ module Mysql2
71
77
  # Timeout::ExitException was removed in Ruby 2.3.0, 2.2.3, and 2.1.8,
72
78
  # but is present in earlier 2.1.x and 2.2.x, so we provide a shim.
73
79
  #
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
80
+ require 'timeout'
81
+ TIMEOUT_ERROR_CLASS = if defined?(::Timeout::ExitException)
82
+ ::Timeout::ExitException
83
+ else
84
+ ::Timeout::Error
82
85
  end
86
+ TIMEOUT_ERROR_NEVER = { TIMEOUT_ERROR_CLASS => :never }.freeze
83
87
  end
84
88
  end
@@ -0,0 +1,49 @@
1
+ -----BEGIN PGP PUBLIC KEY BLOCK-----
2
+ Version: SKS 1.1.6
3
+ Comment: Hostname: pgp.mit.edu
4
+
5
+ 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
47
+ sTSKHe+QnnnoFmu4gnmDU31i
48
+ =Xqbo
49
+ -----END PGP PUBLIC KEY BLOCK-----