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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +1 -0
- data/LICENSE +21 -0
- data/README.md +20 -0
- data/README.org.md +676 -0
- data/lib/mysql2/client.rb +461 -0
- data/lib/mysql2/console.rb +5 -0
- data/lib/mysql2/error.rb +101 -0
- data/lib/mysql2/field.rb +3 -0
- data/lib/mysql2/result.rb +176 -0
- data/lib/mysql2/statement.rb +103 -0
- data/lib/mysql2/version.rb +3 -0
- data/lib/mysql2.rb +87 -0
- metadata +102 -0
@@ -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
|
data/lib/mysql2/error.rb
ADDED
@@ -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
|
data/lib/mysql2/field.rb
ADDED