File: client.rb

package info (click to toggle)
ruby-mysql2 0.5.5-3
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 1,096 kB
  • sloc: ansic: 3,459; ruby: 3,334; sh: 226; makefile: 3
file content (176 lines) | stat: -rw-r--r-- 6,762 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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
module Mysql2
  class Client
    attr_reader :query_options, :read_timeout

    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

    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

      initialize_ext

      # 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, :local_infile, :secure_auth, :automatic_close, :enable_cleartext_plugin
          send(:"#{key}=", !!opts[key]) # rubocop:disable Style/DoubleNegation
        when :connect_timeout, :read_timeout, :write_timeout
          send(:"#{key}=", Integer(opts[key])) unless opts[key].nil?
        else
          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 = opts.values_at(:sslkey, :sslcert, :sslca, :sslcapath, :sslcipher)
      ssl_set(*ssl_options) if ssl_options.any? || opts.key?(:sslverify)
      self.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
    end

    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

    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

    # Find any default system CA paths to handle system roots
    # by default if stricter validation is requested and no
    # path is provide.
    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

    # Set default program_name in performance_schema.session_connect_attrs
    # and performance_schema.session_account_connect_attrs
    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

    def query(sql, options = {})
      Thread.handle_interrupt(::Mysql2::Util::TIMEOUT_ERROR_NEVER) do
        _query(sql, @query_options.merge(options))
      end
    end

    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

    def info
      self.class.info
    end

    class << self
      private

      def local_offset
        ::Time.local(2010).utc_offset.to_r / 86400
      end
    end
  end
end