mysql2 0.3.18 → 0.4.4

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