ruby-mysql2 0.5.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.
@@ -0,0 +1,461 @@
1
+ module Mysql2
2
+ class Client
3
+ Mysql.constants.each do |c|
4
+ case c.to_s
5
+ when /\ACLIENT_/
6
+ self.const_set($', Mysql.const_get(c))
7
+ when /\ASSL_MODE_|\AOPTION_MULTI_STATEMENTS_/
8
+ self.const_set(c, Mysql.const_get(c))
9
+ when /\ASESSION_TRACK_/
10
+ self.const_set(c, Mysql.const_get(c))
11
+ end
12
+ end
13
+
14
+ attr_reader :query_options, :read_timeout
15
+
16
+ def self.default_query_options
17
+ @default_query_options ||= {
18
+ as: :hash, # the type of object you want each row back as; also supports :array (an array of values)
19
+ 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
20
+ cast_booleans: false, # cast tinyint(1) fields as true/false in ruby
21
+ symbolize_keys: false, # return field names as symbols instead of strings
22
+ database_timezone: :local, # timezone Mysql2 will assume datetime objects are stored in
23
+ application_timezone: nil, # timezone Mysql2 will convert to before handing the object back to the caller
24
+ cache_rows: true, # tells Mysql2 to use its internal row cache for results
25
+ connect_flags: REMEMBER_OPTIONS | LONG_PASSWORD | LONG_FLAG | TRANSACTIONS | PROTOCOL_41 | SECURE_CONNECTION | CONNECT_ATTRS,
26
+ cast: true,
27
+ default_file: nil,
28
+ default_group: nil,
29
+ }
30
+ end
31
+
32
+ def self.escape(s)
33
+ s2 = Mysql.quote(s)
34
+ s.size == s2.size ? s : s2
35
+ end
36
+
37
+ def self.info
38
+ {
39
+ id: Mysql::VERSION.split('.').each_with_index.map{|v, i| v.to_i * 100**(2-i)}.sum,
40
+ version: Mysql::VERSION.encode('us-ascii'),
41
+ header_version: Mysql::VERSION.encode('us-ascii'),
42
+ }
43
+ end
44
+
45
+ def self.finalizer(mysql, finalizer_opts)
46
+ proc do
47
+ if finalizer_opts[:automatic_close]
48
+ mysql.close rescue nil
49
+ else
50
+ mysql.close! rescue nil
51
+ end
52
+ end
53
+ end
54
+
55
+ class ::Mysql
56
+ def async_query(str, **)
57
+ check_connection
58
+ @fields = nil
59
+ @protocol.query_command str
60
+ return self
61
+ rescue ServerError => e
62
+ @last_error = e
63
+ @sqlstate = e.sqlstate
64
+ raise
65
+ end
66
+
67
+ def async_query_result(**opts)
68
+ @protocol.get_result
69
+ store_result(**opts)
70
+ rescue ServerError => e
71
+ @last_error = e
72
+ @sqlstate = e.sqlstate
73
+ raise
74
+ end
75
+ end
76
+
77
+ attr_accessor :reconnect
78
+
79
+ def initialize(opts = {})
80
+ raise Mysql2::Error, "Options parameter must be a Hash" unless opts.is_a? Hash
81
+ opts = Mysql2::Util.key_hash_as_symbols(opts)
82
+ @read_timeout = nil
83
+ @query_options = self.class.default_query_options.dup
84
+ @query_options.merge! opts
85
+ @automatic_close = true
86
+
87
+ @mutex = Mutex.new
88
+ @mysql = Mysql.new
89
+
90
+ # Set default connect_timeout to avoid unlimited retries from signal interruption
91
+ opts[:connect_timeout] = 120 unless opts.key?(:connect_timeout)
92
+
93
+ # TODO: stricter validation rather than silent massaging
94
+ %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|
95
+ next unless opts.key?(key)
96
+ case key
97
+ when :reconnect
98
+ send(:"#{key}=", !!opts[key]) # rubocop:disable Style/DoubleNegation
99
+ when :automatic_close
100
+ @automatic_close = opts[key]
101
+ when :local_infile, :secure_auth, :enable_cleartext_plugin
102
+ @mysql.send(:"#{key}=", !!opts[key]) # rubocop:disable Style/DoubleNegation
103
+ when :connect_timeout, :read_timeout, :write_timeout
104
+ t = opts[key]
105
+ if t
106
+ raise Mysql2::Error, 'time interval must not be negative' if t < 0
107
+ t = 0.0000001 if t == 0
108
+ @mysql.send(:"#{key}=", t)
109
+ end
110
+ else
111
+ @mysql.send(:"#{key}=", opts[key])
112
+ end
113
+ end
114
+
115
+ # force the encoding to utf8
116
+ self.charset_name = opts[:encoding] || 'utf8'
117
+
118
+ mode = parse_ssl_mode(opts[:ssl_mode]) if opts[:ssl_mode]
119
+ if (mode == SSL_MODE_VERIFY_CA || mode == SSL_MODE_VERIFY_IDENTITY) && !opts[:sslca]
120
+ opts[:sslca] = find_default_ca_path
121
+ end
122
+
123
+ ssl_options = {}
124
+ ssl_options[:key] = OpenSSL::PKey::RSA.new(File.read(opts[:sslkey])) if opts[:sslkey]
125
+ ssl_options[:cert] = OpenSSL::X509::Certificate.new(File.read(opts[:sslcert])) if opts[:sslcert]
126
+ ssl_options[:ca_file] = opts[:sslca] if opts[:sslca]
127
+ ssl_options[:ca_path] = opts[:sslcapath] if opts[:sslcapath]
128
+ ssl_options[:ciphers] = opts[:sslcipher] if opts[:sslcipher]
129
+ @mysql.ssl_context_params = ssl_options if ssl_options.any? || opts.key?(:sslverify)
130
+ @mysql.ssl_mode = mode if mode
131
+
132
+ flags = case opts[:flags]
133
+ when Array
134
+ parse_flags_array(opts[:flags], @query_options[:connect_flags])
135
+ when String
136
+ parse_flags_array(opts[:flags].split(' '), @query_options[:connect_flags])
137
+ when Integer
138
+ @query_options[:connect_flags] | opts[:flags]
139
+ else
140
+ @query_options[:connect_flags]
141
+ end
142
+
143
+ # SSL verify is a connection flag rather than a mysql_ssl_set option
144
+ flags |= SSL_VERIFY_SERVER_CERT if opts[:sslverify]
145
+
146
+ if %i[user pass hostname dbname db sock].any? { |k| @query_options.key?(k) }
147
+ warn "============= WARNING FROM mysql2 ============="
148
+ warn "The options :user, :pass, :hostname, :dbname, :db, and :sock are deprecated and will be removed at some point in the future."
149
+ warn "Instead, please use :username, :password, :host, :port, :database, :socket, :flags for the options."
150
+ warn "============= END WARNING FROM mysql2 ========="
151
+ end
152
+
153
+ user = opts[:username] || opts[:user]
154
+ pass = opts[:password] || opts[:pass]
155
+ host = opts[:host] || opts[:hostname]
156
+ port = opts[:port]
157
+ database = opts[:database] || opts[:dbname] || opts[:db]
158
+ socket = opts[:socket] || opts[:sock]
159
+
160
+ # Correct the data types before passing these values down to the C level
161
+ user = user.to_s unless user.nil?
162
+ pass = pass.to_s unless pass.nil?
163
+ host = host.to_s unless host.nil?
164
+ port = port.to_i unless port.nil?
165
+ database = database.to_s unless database.nil?
166
+ socket = socket.to_s unless socket.nil?
167
+ conn_attrs = parse_connect_attrs(opts[:connect_attrs])
168
+
169
+ connect user, pass, host, port, database, socket, flags, conn_attrs
170
+ @finalizer_opts = {automatic_close: @automatic_close}
171
+ ObjectSpace.define_finalizer(self, self.class.finalizer(@mysql, @finalizer_opts))
172
+ rescue Mysql::Error => e
173
+ raise Mysql2::Error.new(e.message, @mysql&.server_version, e.errno, e.sqlstate)
174
+ rescue Errno::ECONNREFUSED => e
175
+ raise Mysql2::Error::ConnectionError, e.message
176
+ end
177
+
178
+ def connect(user, pass, host, port, database, socket, flags, conn_attrs)
179
+ @conn_params ||= [ user, pass, host, port, database, socket, flags, conn_attrs ]
180
+ @mysql.connect(host, user, pass, database, port, socket, flags, connect_attrs: conn_attrs)
181
+ rescue Mysql::Error => e
182
+ raise Mysql2::Error::ConnectionError, e.message
183
+ end
184
+
185
+ def close
186
+ @mysql.close rescue nil
187
+ nil
188
+ end
189
+
190
+ def closed?
191
+ s = @mysql.protocol&.instance_variable_get(:@socket)
192
+ s ? s.closed? : true
193
+ end
194
+
195
+ def charset_name=(cs)
196
+ unless cs.is_a? String
197
+ raise TypeError, "wrong argument type Symbol (expected String)"
198
+ end
199
+ @mysql.charset = cs
200
+ rescue Mysql::Error => e
201
+ raise Mysql2::Error.new(e.message, @mysql&.server_version, e.errno, e.sqlstate)
202
+ end
203
+
204
+ def encoding
205
+ Mysql::Charset::CHARSET_ENCODING[@mysql.character_set_name]
206
+ end
207
+
208
+ def escape(s)
209
+ self.class.escape(s)
210
+ end
211
+
212
+ def more_results?
213
+ @mysql.more_results?
214
+ end
215
+
216
+ def next_result
217
+ !! @mysql.next_result(return_result: false)
218
+ rescue Mysql::Error => e
219
+ raise Mysql2::Error.new(e.message, @mysql&.server_version, e.errno, e.sqlstate)
220
+ end
221
+
222
+ def abandon_results!
223
+ while more_results?
224
+ next_result
225
+ store_result
226
+ end
227
+ end
228
+
229
+ def store_result
230
+ res = @mysql.store_result
231
+ reset_active_thread
232
+ res && Result.new(res, @current_query_options)
233
+ end
234
+
235
+ def async_result
236
+ current_thread_active?
237
+ res = @mysql.async_query_result
238
+ reset_active_thread
239
+ Result.new(res, @current_query_options)
240
+ end
241
+
242
+ def affected_rows
243
+ @mysql.affected_rows
244
+ end
245
+
246
+ def parse_ssl_mode(mode)
247
+ m = mode.to_s.upcase
248
+ if m.start_with?('SSL_MODE_')
249
+ return Mysql2::Client.const_get(m) if Mysql2::Client.const_defined?(m)
250
+ else
251
+ x = 'SSL_MODE_' + m
252
+ return Mysql2::Client.const_get(x) if Mysql2::Client.const_defined?(x)
253
+ end
254
+ warn "Unknown MySQL ssl_mode flag: #{mode}"
255
+ end
256
+
257
+ def ssl_cipher
258
+ @mysql.protocol&.ssl_cipher&.first
259
+ end
260
+
261
+ def parse_flags_array(flags, initial = 0)
262
+ flags.reduce(initial) do |memo, f|
263
+ fneg = f.start_with?('-') ? f[1..-1] : nil
264
+ if fneg && fneg =~ /^\w+$/ && Mysql2::Client.const_defined?(fneg)
265
+ memo & ~ Mysql2::Client.const_get(fneg)
266
+ elsif f && f =~ /^\w+$/ && Mysql2::Client.const_defined?(f)
267
+ memo | Mysql2::Client.const_get(f)
268
+ else
269
+ warn "Unknown MySQL connection flag: '#{f}'"
270
+ memo
271
+ end
272
+ end
273
+ end
274
+
275
+ # Find any default system CA paths to handle system roots
276
+ # by default if stricter validation is requested and no
277
+ # path is provide.
278
+ def find_default_ca_path
279
+ [
280
+ "/etc/ssl/certs/ca-certificates.crt",
281
+ "/etc/pki/tls/certs/ca-bundle.crt",
282
+ "/etc/ssl/ca-bundle.pem",
283
+ "/etc/ssl/cert.pem",
284
+ ].find { |f| File.exist?(f) }
285
+ end
286
+
287
+ # Set default program_name in performance_schema.session_connect_attrs
288
+ # and performance_schema.session_account_connect_attrs
289
+ def parse_connect_attrs(conn_attrs)
290
+ return {} if Mysql2::Client::CONNECT_ATTRS.zero?
291
+ conn_attrs ||= {}
292
+ conn_attrs[:program_name] ||= $PROGRAM_NAME
293
+ conn_attrs.each_with_object({}) do |(key, value), hash|
294
+ hash[key.to_s] = value.to_s
295
+ end
296
+ end
297
+
298
+ def set_active_thread
299
+ @mutex.synchronize do
300
+ if @active_thread
301
+ if @active_thread == Thread.current
302
+ raise Mysql2::Error, 'This connection is still waiting for a result, try again once you have the result'
303
+ else
304
+ raise Mysql2::Error, "This connection is in use by: #{@active_thread.inspect}"
305
+ end
306
+ end
307
+ @active_thread = Thread.current
308
+ end
309
+ end
310
+
311
+ def reset_active_thread
312
+ @mutex.synchronize do
313
+ @active_thread = nil
314
+ end
315
+ end
316
+
317
+ def current_thread_active?
318
+ unless Thread.current == @active_thread
319
+ raise Mysql2::Error, 'This connection is still waiting for a result, try again once you have the result'
320
+ end
321
+ end
322
+
323
+ def query(sql, options = {})
324
+ if @reconnect && @mysql.protocol && closed?
325
+ connect(*@conn_params)
326
+ end
327
+ raise TypeError, "wrong argument type #{sql.class} (expected String)" unless sql.is_a? String
328
+ raise Mysql2::Error, 'MySQL client is not connected' if closed?
329
+ set_active_thread
330
+ @current_query_options = @query_options.merge(options)
331
+ Thread.handle_interrupt(::Mysql2::Util::TIMEOUT_ERROR_NEVER) do
332
+ if options[:async]
333
+ @mysql.async_query(sql, auto_store_result: !options[:stream], **options)
334
+ return nil
335
+ end
336
+ res = @mysql.query(sql, auto_store_result: !options[:stream], **options)
337
+ reset_active_thread
338
+ return res && Result.new(res, @current_query_options)
339
+ end
340
+ rescue Mysql::Error => e
341
+ reset_active_thread
342
+ if e.message =~ /timeout$/
343
+ raise Mysql2::Error::TimeoutError, e.message
344
+ else
345
+ raise Mysql2::Error.new(e.message, @mysql&.server_version, e.errno, e.sqlstate)
346
+ end
347
+ rescue Errno::ENOENT => e
348
+ reset_active_thread
349
+ raise Mysql2::Error, e.message
350
+ rescue
351
+ reset_active_thread
352
+ @mysql&.protocol&.close
353
+ raise
354
+ end
355
+
356
+ def row_to_hash(res, fields, &block)
357
+ res.each do |row|
358
+ h = {}
359
+ fields.each_with_index do |f, i|
360
+ key = @current_query_options[:symbolize_keys] ? f.name.intern : f.name
361
+ h[key] = convert_type(row[i], f.type)
362
+ end
363
+ block.call h
364
+ end
365
+ end
366
+
367
+ def query_info
368
+ info = query_info_string
369
+ return {} unless info
370
+ info_hash = {}
371
+ info.split.each_slice(2) { |s| info_hash[s[0].downcase.delete(':').to_sym] = s[1].to_i }
372
+ info_hash
373
+ end
374
+
375
+ def query_info_string
376
+ @mysql.info
377
+ end
378
+
379
+ def info
380
+ self.class.info
381
+ end
382
+
383
+ def server_info
384
+ {
385
+ id: @mysql.server_version,
386
+ version: @mysql.server_info.encode(Encoding.default_internal || encoding),
387
+ }
388
+ rescue => e
389
+ raise Mysql2::Error, e.message
390
+ end
391
+
392
+ def last_id
393
+ @mysql.insert_id
394
+ end
395
+
396
+ def ping
397
+ @mysql.ping
398
+ true
399
+ rescue
400
+ false
401
+ end
402
+
403
+ def thread_id
404
+ @mysql.thread_id
405
+ end
406
+
407
+ def select_db(db)
408
+ query("use `#{db}`")
409
+ db
410
+ end
411
+
412
+ def set_server_option(opt)
413
+ @mysql.set_server_option(opt)
414
+ true
415
+ rescue Mysql::Error => e
416
+ return false if e.message == 'Unknown command'
417
+ raise Mysql2::Error.new(e.message, @mysql&.server_version, e.errno, e.sqlstate)
418
+ end
419
+
420
+ def warning_count
421
+ @mysql.warning_count
422
+ end
423
+
424
+ def socket
425
+ raise Mysql2::Error, 'MySQL client is not connected' if closed?
426
+ @mysql.protocol.instance_variable_get(:@socket).fileno
427
+ end
428
+
429
+ def automatic_close
430
+ @automatic_close
431
+ end
432
+
433
+ def automatic_close=(f)
434
+ @automatic_close = f
435
+ @finalizer_opts[:automatic_close] = f
436
+ end
437
+
438
+ def automatic_close?
439
+ @automatic_close ? true : false
440
+ end
441
+
442
+ def prepare(*args)
443
+ st = @mysql.prepare(*args)
444
+ Statement.new(st, **@query_options)
445
+ rescue Mysql::Error => e
446
+ raise Mysql2::Error.new(e.message, @mysql&.server_version, e.errno, e.sqlstate)
447
+ end
448
+
449
+ def session_track(type)
450
+ @mysql.session_track[type]&.flatten
451
+ end
452
+
453
+ class << self
454
+ private
455
+
456
+ def local_offset
457
+ ::Time.local(2010).utc_offset.to_r / 86400
458
+ end
459
+ end
460
+ end
461
+ end
@@ -0,0 +1,5 @@
1
+ # Loaded by script/console. Land helpers here.
2
+
3
+ Pry.config.prompt = lambda do |context, *|
4
+ "[mysql2] #{context}> "
5
+ end
@@ -0,0 +1,101 @@
1
+ module Mysql2
2
+ class Error < StandardError
3
+ ENCODE_OPTS = {
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
45
+ }.freeze
46
+
47
+ attr_reader :error_number, :sql_state
48
+
49
+ # Mysql gem compatibility
50
+ alias errno error_number
51
+ alias error message
52
+
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('ascii', **ENCODE_OPTS) : nil
57
+
58
+ super(clean_message(msg))
59
+ end
60
+
61
+ def self.new_with_args(msg, server_version, error_number, sql_state)
62
+ error_class = CODES.fetch(error_number, self)
63
+ error_class.new(msg, server_version, error_number, sql_state)
64
+ end
65
+
66
+ private
67
+
68
+ # In MySQL 5.5+ error messages are always constructed server-side as UTF-8
69
+ # then returned in the encoding set by the `character_set_results` system
70
+ # variable.
71
+ #
72
+ # See http://dev.mysql.com/doc/refman/5.5/en/charset-errors.html for
73
+ # more context.
74
+ #
75
+ # Before MySQL 5.5 error message template strings are in whatever encoding
76
+ # is associated with the error message language.
77
+ # See http://dev.mysql.com/doc/refman/5.1/en/error-message-language.html
78
+ # for more information.
79
+ #
80
+ # The issue is that the user-data inserted in the message could potentially
81
+ # be in any encoding MySQL supports and is insert into the latin1, euckr or
82
+ # koi8r string raw. Meaning there's a high probability the string will be
83
+ # corrupt encoding-wise.
84
+ #
85
+ # See http://dev.mysql.com/doc/refman/5.1/en/charset-errors.html for
86
+ # more information.
87
+ #
88
+ # So in an attempt to make sure the error message string is always in a valid
89
+ # encoding, we'll assume UTF-8 and clean the string of anything that's not a
90
+ # valid UTF-8 character.
91
+ #
92
+ # Returns a valid UTF-8 string.
93
+ def clean_message(message)
94
+ if @server_version && @server_version > 50500
95
+ message.encode(**ENCODE_OPTS)
96
+ else
97
+ message.encode(Encoding::UTF_8, **ENCODE_OPTS)
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,3 @@
1
+ module Mysql2
2
+ Field = Struct.new(:name, :type)
3
+ end