ruby-mysql2 0.5.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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