Class: Mysql2::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/mysql2/client.rb

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(opts = {}) ⇒ Client

Returns a new instance of Client.



79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/mysql2/client.rb', line 79

def initialize(opts = {})
  raise Mysql2::Error, "Options parameter must be a Hash" unless opts.is_a? Hash
  opts = Mysql2::Util.key_hash_as_symbols(opts)
  @read_timeout = nil
  @query_options = self.class.default_query_options.dup
  @query_options.merge! opts
  @automatic_close = true

  @mutex = Mutex.new
  @mysql = Mysql.new

  # Set default connect_timeout to avoid unlimited retries from signal interruption
  opts[:connect_timeout] = 120 unless opts.key?(:connect_timeout)

  # TODO: stricter validation rather than silent massaging
  %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|
    next unless opts.key?(key)
    case key
    when :reconnect
      send(:"#{key}=", !!opts[key]) # rubocop:disable Style/DoubleNegation
    when :automatic_close
      @automatic_close = opts[key]
    when :local_infile, :secure_auth, :enable_cleartext_plugin
      @mysql.send(:"#{key}=", !!opts[key]) # rubocop:disable Style/DoubleNegation
    when :connect_timeout, :read_timeout, :write_timeout
      t = opts[key]
      if t
        raise Mysql2::Error, 'time interval must not be negative' if t < 0
        t = 0.0000001 if t == 0
        @mysql.send(:"#{key}=", t)
      end
    else
      @mysql.send(:"#{key}=", opts[key])
    end
  end

  # force the encoding to utf8
  self.charset_name = opts[:encoding] || 'utf8'

  mode = parse_ssl_mode(opts[:ssl_mode]) if opts[:ssl_mode]
  if (mode == SSL_MODE_VERIFY_CA || mode == SSL_MODE_VERIFY_IDENTITY) && !opts[:sslca]
    opts[:sslca] = find_default_ca_path
  end

  ssl_options = {}
  ssl_options[:key] = OpenSSL::PKey::RSA.new(File.read(opts[:sslkey])) if opts[:sslkey]
  ssl_options[:cert] = OpenSSL::X509::Certificate.new(File.read(opts[:sslcert])) if opts[:sslcert]
  ssl_options[:ca_file] = opts[:sslca] if opts[:sslca]
  ssl_options[:ca_path] = opts[:sslcapath] if opts[:sslcapath]
  ssl_options[:ciphers] = opts[:sslcipher] if opts[:sslcipher]
  @mysql.ssl_context_params = ssl_options if ssl_options.any? || opts.key?(:sslverify)
  @mysql.ssl_mode = mode if mode

  flags = case opts[:flags]
  when Array
    parse_flags_array(opts[:flags], @query_options[:connect_flags])
  when String
    parse_flags_array(opts[:flags].split(' '), @query_options[:connect_flags])
  when Integer
    @query_options[:connect_flags] | opts[:flags]
  else
    @query_options[:connect_flags]
  end

  # SSL verify is a connection flag rather than a mysql_ssl_set option
  flags |= SSL_VERIFY_SERVER_CERT if opts[:sslverify]

  if %i[user pass hostname dbname db sock].any? { |k| @query_options.key?(k) }
    warn "============= WARNING FROM mysql2 ============="
    warn "The options :user, :pass, :hostname, :dbname, :db, and :sock are deprecated and will be removed at some point in the future."
    warn "Instead, please use :username, :password, :host, :port, :database, :socket, :flags for the options."
    warn "============= END WARNING FROM mysql2 ========="
  end

  user     = opts[:username] || opts[:user]
  pass     = opts[:password] || opts[:pass]
  host     = opts[:host] || opts[:hostname]
  port     = opts[:port]
  database = opts[:database] || opts[:dbname] || opts[:db]
  socket   = opts[:socket] || opts[:sock]

  # Correct the data types before passing these values down to the C level
  user = user.to_s unless user.nil?
  pass = pass.to_s unless pass.nil?
  host = host.to_s unless host.nil?
  port = port.to_i unless port.nil?
  database = database.to_s unless database.nil?
  socket = socket.to_s unless socket.nil?
  conn_attrs = parse_connect_attrs(opts[:connect_attrs])

  connect user, pass, host, port, database, socket, flags, conn_attrs
  @finalizer_opts = {automatic_close: @automatic_close}
  ObjectSpace.define_finalizer(self, self.class.finalizer(@mysql, @finalizer_opts))
rescue Mysql::Error => e
  raise Mysql2::Error.new(e.message, @mysql&.server_version, e.errno, e.sqlstate)
rescue Errno::ECONNREFUSED => e
  raise Mysql2::Error::ConnectionError, e.message
end

Instance Attribute Details

#query_optionsObject (readonly)

Returns the value of attribute query_options.



14
15
16
# File 'lib/mysql2/client.rb', line 14

def query_options
  @query_options
end

#read_timeoutObject (readonly)

Returns the value of attribute read_timeout.



14
15
16
# File 'lib/mysql2/client.rb', line 14

def read_timeout
  @read_timeout
end

#reconnectObject

Returns the value of attribute reconnect.



77
78
79
# File 'lib/mysql2/client.rb', line 77

def reconnect
  @reconnect
end

Class Method Details

.default_query_optionsObject



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# File 'lib/mysql2/client.rb', line 16

def self.default_query_options
  @default_query_options ||= {
    as: :hash,                   # the type of object you want each row back as; also supports :array (an array of values)
    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
    cast_booleans: false,        # cast tinyint(1) fields as true/false in ruby
    symbolize_keys: false,       # return field names as symbols instead of strings
    database_timezone: :local,   # timezone Mysql2 will assume datetime objects are stored in
    application_timezone: nil,   # timezone Mysql2 will convert to before handing the object back to the caller
    cache_rows: true,            # tells Mysql2 to use its internal row cache for results
    connect_flags: REMEMBER_OPTIONS | LONG_PASSWORD | LONG_FLAG | TRANSACTIONS | PROTOCOL_41 | SECURE_CONNECTION | CONNECT_ATTRS,
    cast: true,
    default_file: nil,
    default_group: nil,
  }
end

.escape(s) ⇒ Object



32
33
34
35
# File 'lib/mysql2/client.rb', line 32

def self.escape(s)
  s2 = Mysql.quote(s)
  s.size == s2.size ? s : s2
end

.finalizer(mysql, finalizer_opts) ⇒ Object



45
46
47
48
49
50
51
52
53
# File 'lib/mysql2/client.rb', line 45

def self.finalizer(mysql, finalizer_opts)
  proc do
    if finalizer_opts[:automatic_close]
      mysql.close rescue nil
    else
      mysql.close! rescue nil
    end
  end
end

.infoObject



37
38
39
40
41
42
43
# File 'lib/mysql2/client.rb', line 37

def self.info
  {
    id: Mysql::VERSION.split('.').each_with_index.map{|v, i| v.to_i * 100**(2-i)}.sum,
    version: Mysql::VERSION.encode('us-ascii'),
    header_version: Mysql::VERSION.encode('us-ascii'),
  }
end

Instance Method Details

#abandon_results!Object



222
223
224
225
226
227
# File 'lib/mysql2/client.rb', line 222

def abandon_results!
  while more_results?
    next_result
    store_result
  end
end

#affected_rowsObject



242
243
244
# File 'lib/mysql2/client.rb', line 242

def affected_rows
  @mysql.affected_rows
end

#async_resultObject



235
236
237
238
239
240
# File 'lib/mysql2/client.rb', line 235

def async_result
  current_thread_active?
  res = @mysql.async_query_result
  reset_active_thread
  Result.new(res, @current_query_options)
end

#automatic_closeObject



429
430
431
# File 'lib/mysql2/client.rb', line 429

def automatic_close
  @automatic_close
end

#automatic_close=(f) ⇒ Object



433
434
435
436
# File 'lib/mysql2/client.rb', line 433

def automatic_close=(f)
  @automatic_close = f
  @finalizer_opts[:automatic_close] = f
end

#automatic_close?Boolean

Returns:

  • (Boolean)


438
439
440
# File 'lib/mysql2/client.rb', line 438

def automatic_close?
  @automatic_close ? true : false
end

#charset_name=(cs) ⇒ Object



195
196
197
198
199
200
201
202
# File 'lib/mysql2/client.rb', line 195

def charset_name=(cs)
  unless cs.is_a? String
    raise TypeError, "wrong argument type Symbol (expected String)"
  end
  @mysql.charset = cs
rescue Mysql::Error => e
  raise Mysql2::Error.new(e.message, @mysql&.server_version, e.errno, e.sqlstate)
end

#closeObject



185
186
187
188
# File 'lib/mysql2/client.rb', line 185

def close
  @mysql.close rescue nil
  nil
end

#closed?Boolean

Returns:

  • (Boolean)


190
191
192
193
# File 'lib/mysql2/client.rb', line 190

def closed?
  s = @mysql.protocol&.instance_variable_get(:@socket)
  s ? s.closed? : true
end

#connect(user, pass, host, port, database, socket, flags, conn_attrs) ⇒ Object



178
179
180
181
182
183
# File 'lib/mysql2/client.rb', line 178

def connect(user, pass, host, port, database, socket, flags, conn_attrs)
  @conn_params ||= [ user, pass, host, port, database, socket, flags, conn_attrs ]
  @mysql.connect(host, user, pass, database, port, socket, flags, connect_attrs: conn_attrs)
rescue Mysql::Error => e
  raise Mysql2::Error::ConnectionError, e.message
end

#current_thread_active?Boolean

Returns:

  • (Boolean)


317
318
319
320
321
# File 'lib/mysql2/client.rb', line 317

def current_thread_active?
  unless Thread.current == @active_thread
    raise Mysql2::Error, 'This connection is still waiting for a result, try again once you have the result'
  end
end

#encodingObject



204
205
206
# File 'lib/mysql2/client.rb', line 204

def encoding
  Mysql::Charset::CHARSET_ENCODING[@mysql.character_set_name]
end

#escape(s) ⇒ Object



208
209
210
# File 'lib/mysql2/client.rb', line 208

def escape(s)
  self.class.escape(s)
end

#find_default_ca_pathObject

Find any default system CA paths to handle system roots by default if stricter validation is requested and no path is provide.



278
279
280
281
282
283
284
285
# File 'lib/mysql2/client.rb', line 278

def find_default_ca_path
  [
    "/etc/ssl/certs/ca-certificates.crt",
    "/etc/pki/tls/certs/ca-bundle.crt",
    "/etc/ssl/ca-bundle.pem",
    "/etc/ssl/cert.pem",
  ].find { |f| File.exist?(f) }
end

#infoObject



379
380
381
# File 'lib/mysql2/client.rb', line 379

def info
  self.class.info
end

#last_idObject



392
393
394
# File 'lib/mysql2/client.rb', line 392

def last_id
  @mysql.insert_id
end

#more_results?Boolean

Returns:

  • (Boolean)


212
213
214
# File 'lib/mysql2/client.rb', line 212

def more_results?
  @mysql.more_results?
end

#next_resultObject



216
217
218
219
220
# File 'lib/mysql2/client.rb', line 216

def next_result
  !! @mysql.next_result(return_result: false)
rescue Mysql::Error => e
  raise Mysql2::Error.new(e.message, @mysql&.server_version, e.errno, e.sqlstate)
end

#parse_connect_attrs(conn_attrs) ⇒ Object

Set default program_name in performance_schema.session_connect_attrs and performance_schema.session_account_connect_attrs



289
290
291
292
293
294
295
296
# File 'lib/mysql2/client.rb', line 289

def parse_connect_attrs(conn_attrs)
  return {} if Mysql2::Client::CONNECT_ATTRS.zero?
  conn_attrs ||= {}
  conn_attrs[:program_name] ||= $PROGRAM_NAME
  conn_attrs.each_with_object({}) do |(key, value), hash|
    hash[key.to_s] = value.to_s
  end
end

#parse_flags_array(flags, initial = 0) ⇒ Object



261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/mysql2/client.rb', line 261

def parse_flags_array(flags, initial = 0)
  flags.reduce(initial) do |memo, f|
    fneg = f.start_with?('-') ? f[1..-1] : nil
    if fneg && fneg =~ /^\w+$/ && Mysql2::Client.const_defined?(fneg)
      memo & ~ Mysql2::Client.const_get(fneg)
    elsif f && f =~ /^\w+$/ && Mysql2::Client.const_defined?(f)
      memo | Mysql2::Client.const_get(f)
    else
      warn "Unknown MySQL connection flag: '#{f}'"
      memo
    end
  end
end

#parse_ssl_mode(mode) ⇒ Object



246
247
248
249
250
251
252
253
254
255
# File 'lib/mysql2/client.rb', line 246

def parse_ssl_mode(mode)
  m = mode.to_s.upcase
  if m.start_with?('SSL_MODE_')
    return Mysql2::Client.const_get(m) if Mysql2::Client.const_defined?(m)
  else
    x = 'SSL_MODE_' + m
    return Mysql2::Client.const_get(x) if Mysql2::Client.const_defined?(x)
  end
  warn "Unknown MySQL ssl_mode flag: #{mode}"
end

#pingObject



396
397
398
399
400
401
# File 'lib/mysql2/client.rb', line 396

def ping
  @mysql.ping
  true
rescue
  false
end

#prepare(*args) ⇒ Object



442
443
444
445
446
447
# File 'lib/mysql2/client.rb', line 442

def prepare(*args)
  st = @mysql.prepare(*args)
  Statement.new(st, **@query_options)
rescue Mysql::Error => e
  raise Mysql2::Error.new(e.message, @mysql&.server_version, e.errno, e.sqlstate)
end

#query(sql, options = {}) ⇒ Object



323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
# File 'lib/mysql2/client.rb', line 323

def query(sql, options = {})
  if @reconnect && @mysql.protocol && closed?
    connect(*@conn_params)
  end
  raise TypeError, "wrong argument type #{sql.class} (expected String)" unless sql.is_a? String
  raise Mysql2::Error, 'MySQL client is not connected' if closed?
  set_active_thread
  @current_query_options = @query_options.merge(options)
  Thread.handle_interrupt(::Mysql2::Util::TIMEOUT_ERROR_NEVER) do
    if options[:async]
      @mysql.async_query(sql, auto_store_result: !options[:stream], **options)
      return nil
    end
    res = @mysql.query(sql, auto_store_result: !options[:stream], **options)
    reset_active_thread
    return res && Result.new(res, @current_query_options)
  end
rescue Mysql::Error => e
  reset_active_thread
  if e.message =~ /timeout$/
    raise Mysql2::Error::TimeoutError, e.message
  else
    raise Mysql2::Error.new(e.message, @mysql&.server_version, e.errno, e.sqlstate)
  end
rescue Errno::ENOENT => e
  reset_active_thread
  raise Mysql2::Error, e.message
rescue
  reset_active_thread
  @mysql&.protocol&.close
  raise
end

#query_infoObject



367
368
369
370
371
372
373
# File 'lib/mysql2/client.rb', line 367

def query_info
  info = query_info_string
  return {} unless info
  info_hash = {}
  info.split.each_slice(2) { |s| info_hash[s[0].downcase.delete(':').to_sym] = s[1].to_i }
  info_hash
end

#query_info_stringObject



375
376
377
# File 'lib/mysql2/client.rb', line 375

def query_info_string
  @mysql.info
end

#reset_active_threadObject



311
312
313
314
315
# File 'lib/mysql2/client.rb', line 311

def reset_active_thread
  @mutex.synchronize do
    @active_thread = nil
  end
end

#row_to_hash(res, fields, &block) ⇒ Object



356
357
358
359
360
361
362
363
364
365
# File 'lib/mysql2/client.rb', line 356

def row_to_hash(res, fields, &block)
  res.each do |row|
    h = {}
    fields.each_with_index do |f, i|
      key = @current_query_options[:symbolize_keys] ? f.name.intern : f.name
      h[key] = convert_type(row[i], f.type)
    end
    block.call h
  end
end

#select_db(db) ⇒ Object



407
408
409
410
# File 'lib/mysql2/client.rb', line 407

def select_db(db)
  query("use `#{db}`")
  db
end

#server_infoObject



383
384
385
386
387
388
389
390
# File 'lib/mysql2/client.rb', line 383

def server_info
  {
    id: @mysql.server_version,
    version: @mysql.server_info.encode(Encoding.default_internal || encoding),
  }
rescue => e
  raise Mysql2::Error, e.message
end

#session_track(type) ⇒ Object



449
450
451
# File 'lib/mysql2/client.rb', line 449

def session_track(type)
  @mysql.session_track[type]&.flatten
end

#set_active_threadObject



298
299
300
301
302
303
304
305
306
307
308
309
# File 'lib/mysql2/client.rb', line 298

def set_active_thread
  @mutex.synchronize do
    if @active_thread
      if @active_thread == Thread.current
        raise Mysql2::Error, 'This connection is still waiting for a result, try again once you have the result'
      else
        raise Mysql2::Error, "This connection is in use by: #{@active_thread.inspect}"
      end
    end
    @active_thread = Thread.current
  end
end

#set_server_option(opt) ⇒ Object



412
413
414
415
416
417
418
# File 'lib/mysql2/client.rb', line 412

def set_server_option(opt)
  @mysql.set_server_option(opt)
  true
rescue Mysql::Error => e
  return false if e.message == 'Unknown command'
  raise Mysql2::Error.new(e.message, @mysql&.server_version, e.errno, e.sqlstate)
end

#socketObject

Raises:



424
425
426
427
# File 'lib/mysql2/client.rb', line 424

def socket
  raise Mysql2::Error, 'MySQL client is not connected' if closed?
  @mysql.protocol.instance_variable_get(:@socket).fileno
end

#ssl_cipherObject



257
258
259
# File 'lib/mysql2/client.rb', line 257

def ssl_cipher
  @mysql.protocol&.ssl_cipher&.first
end

#store_resultObject



229
230
231
232
233
# File 'lib/mysql2/client.rb', line 229

def store_result
  res = @mysql.store_result
  reset_active_thread
  res && Result.new(res, @current_query_options)
end

#thread_idObject



403
404
405
# File 'lib/mysql2/client.rb', line 403

def thread_id
  @mysql.thread_id
end

#warning_countObject



420
421
422
# File 'lib/mysql2/client.rb', line 420

def warning_count
  @mysql.warning_count
end