Class: Exwiw::Adapter::MysqlClient

Inherits:
Object
  • Object
show all
Defined in:
lib/exwiw/adapter/mysql_client.rb

Overview

Thin wrapper over the MySQL driver so MysqlAdapter does not care whether the host app ships the ‘mysql2` gem or the `trilogy` gem. exwiw only runs simple SELECT / EXPLAIN queries, so both drivers are normalized to the same shape: rows as arrays of String|nil plus the column names.

Values are normalized to strings to match mysql2’s ‘cast: false` mode, where every column comes back as a raw string and is quoted uniformly downstream (see MysqlAdapter#escape_value). mysql2 already returns strings in that mode; trilogy always casts to Ruby types (Integer / BigDecimal / Time / Date / …), so its values are stringified back into the same literal form here.

Defined Under Namespace

Classes: Result

Constant Summary collapse

DRIVERS =
[:mysql2, :trilogy].freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(connection_config, driver: nil) ⇒ MysqlClient

‘driver:` is mainly a test seam to force a specific driver; in normal use it is auto-detected.



98
99
100
101
102
# File 'lib/exwiw/adapter/mysql_client.rb', line 98

def initialize(connection_config, driver: nil)
  @connection_config = connection_config
  @driver = (driver || self.class.detect_driver).to_sym
  ensure_driver_loaded!
end

Instance Attribute Details

#driverObject (readonly)

Returns the value of attribute driver.



94
95
96
# File 'lib/exwiw/adapter/mysql_client.rb', line 94

def driver
  @driver
end

Class Method Details

.detect_driverObject

Pick the available driver, preferring mysql2 (exwiw’s historical default). require returns false when already loaded, so this is safe to call repeatedly.

Set EXWIW_MYSQL_DRIVER=trilogy to force the pure-Ruby trilogy driver. This is useful when the mysql2 gem is linked against a libmysqlclient that can no longer load the server’s auth plugin (e.g. MySQL 9.x client dropped the ‘mysql_native_password` plugin .so, raising “Authentication plugin ’mysql_native_password’ cannot be loaded” on connect).



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/exwiw/adapter/mysql_client.rb', line 31

def self.detect_driver
  forced = ENV['EXWIW_MYSQL_DRIVER']
  if forced && !forced.empty?
    sym = forced.to_sym
    unless DRIVERS.include?(sym)
      raise ArgumentError,
            "EXWIW_MYSQL_DRIVER must be one of #{DRIVERS.join(', ')}, got #{forced.inspect}."
    end
    return sym
  end

  require 'mysql2'
  :mysql2
rescue LoadError
  begin
    require 'trilogy'
    :trilogy
  rescue LoadError
    raise LoadError,
          "exwiw needs the 'mysql2' or 'trilogy' gem to connect to MySQL. " \
          "Add `gem \"mysql2\"` (or `gem \"trilogy\"`) to your Gemfile."
  end
end

.normalize_encoding(str) ⇒ Object

Re-tag a value string as UTF-8 when it comes back as ASCII-8BIT (BINARY). Both drivers tag values from binary-collation / VARBINARY / BLOB columns as ASCII-8BIT even when the bytes are really UTF-8 text. When exwiw runs inside a host process whose Encoding.default_internal is UTF-8 (e.g. a Rails app, or RUBYOPT=-EUTF-8), IO#write enables conversion, so writing such a binary string carrying multi-byte bytes (e.g. Japanese “xE4…”) to the INSERT file raises “xE4 from ASCII-8BIT to UTF-8” (Encoding::UndefinedConversionError). Re-tagging makes that write a UTF-8 -> UTF-8 no-op; only the tag changes, the bytes pass through.



88
89
90
91
92
# File 'lib/exwiw/adapter/mysql_client.rb', line 88

def self.normalize_encoding(str)
  return str unless str.encoding == Encoding::ASCII_8BIT

  str.dup.force_encoding(Encoding::UTF_8)
end

.stringify_value(value) ⇒ Object

Render a driver-returned value as the raw string mysql2’s ‘cast: false` would have produced, so trilogy’s typed values quote identically.



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/exwiw/adapter/mysql_client.rb', line 57

def self.stringify_value(value)
  case value
  when nil then nil
  when String then normalize_encoding(value)
  when Time
    # Emit fractional seconds only when present. A Time can't tell us the
    # column's declared precision, so a zero fraction on a DATETIME(6)
    # column comes out as "...:00" here whereas mysql2's `cast: false`
    # echoes the raw "...:00.000000"; both re-insert to the same instant.
    value.nsec.zero? ? value.strftime('%Y-%m-%d %H:%M:%S') : value.strftime('%Y-%m-%d %H:%M:%S.%6N')
  when Date then value.strftime('%Y-%m-%d')
  when true then '1'
  when false then '0'
  else
    if defined?(BigDecimal) && value.is_a?(BigDecimal)
      value.to_s('F')
    else
      value.to_s
    end
  end
end

Instance Method Details

#query(sql) ⇒ Result

Returns fields (Array<String>) and rows (Array<Array<String|nil>>).

Parameters:

  • sql (String)

Returns:

  • (Result)

    fields (Array<String>) and rows (Array<Array<String|nil>>)



106
107
108
109
110
111
112
113
114
115
116
117
118
119
# File 'lib/exwiw/adapter/mysql_client.rb', line 106

def query(sql)
  case @driver
  when :mysql2
    res = raw.query(sql, cast: false, as: :array)
    rows = res.to_a.map { |row| row.map { |value| self.class.stringify_value(value) } }
    Result.new(res.fields, rows)
  when :trilogy
    res = raw.query(sql)
    rows = res.rows.map { |row| row.map { |value| self.class.stringify_value(value) } }
    Result.new(res.fields, rows)
  else
    raise "Unsupported MySQL driver: #{@driver.inspect}"
  end
end