├── .document ├── README.md ├── .github └── workflows │ ├── mysqld.cnf │ └── test.yml ├── Gemfile ├── bench ├── 05_many_query.rb ├── 03_select.rb ├── 04_select_prepare.rb ├── 01_insert.rb ├── 02_insert_prepare.rb └── run.rb ├── .autotest ├── Gemfile.lock ├── Rakefile ├── ruby-mysql.gemspec ├── LICENSE ├── lib ├── mysql │ ├── authenticator │ │ ├── mysql_native_password.rb │ │ ├── sha256_password.rb │ │ └── caching_sha2_password.rb │ ├── packet.rb │ ├── authenticator.rb │ ├── constants.rb │ ├── charset.rb │ └── protocol.rb └── mysql.rb ├── CHANGELOG.md ├── test ├── test_mysql_packet.rb └── test_mysql.rb └── setup.rb /.document: -------------------------------------------------------------------------------- 1 | --no-private 2 | lib/mysql.rb 3 | lib/mysql/charset.rb 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ruby-mysql moved to GitLab 2 | 3 | 4 | -------------------------------------------------------------------------------- /.github/workflows/mysqld.cnf: -------------------------------------------------------------------------------- 1 | [mysqld] 2 | local-infile = true 3 | max-allowed-packet = 100000000 4 | innodb-log-files-in-group = 10 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'http://rubygems.org' 2 | 3 | group :development do 4 | gem 'rake' 5 | gem 'test-unit' 6 | gem 'test-unit-rr' 7 | end 8 | -------------------------------------------------------------------------------- /bench/05_many_query.rb: -------------------------------------------------------------------------------- 1 | m = Mysql.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 2 | 100000.times do 3 | m.query('set @a=1') 4 | end 5 | -------------------------------------------------------------------------------- /.autotest: -------------------------------------------------------------------------------- 1 | Autotest.add_hook :initialize do |at| 2 | at.add_exception(/\.git/) 3 | at.add_exception(/\/doc\//) 4 | at.add_mapping(/lib\/.*\.rb$/, true) do |f, _| 5 | Dir.glob 'spec/**/*.rb' 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /bench/03_select.rb: -------------------------------------------------------------------------------- 1 | m = Mysql.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 2 | 10.times do 3 | m.query((['select * from bench_test']*100).join(' union all ')).each{} 4 | end 5 | -------------------------------------------------------------------------------- /bench/04_select_prepare.rb: -------------------------------------------------------------------------------- 1 | m = Mysql.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 2 | p = m.prepare((['select * from bench_test']*100).join(' union all ')) 3 | 10.times do 4 | p.execute.each{} 5 | end 6 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: http://rubygems.org/ 3 | specs: 4 | power_assert (2.0.1) 5 | rake (13.0.6) 6 | rr (3.0.9) 7 | test-unit (3.5.3) 8 | power_assert 9 | test-unit-rr (1.0.5) 10 | rr (>= 1.1.1) 11 | test-unit (>= 2.5.2) 12 | 13 | PLATFORMS 14 | ruby 15 | 16 | DEPENDENCIES 17 | rake 18 | test-unit 19 | test-unit-rr 20 | 21 | BUNDLED WITH 22 | 2.3.7 23 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # @example 2 | # rake test MYSQL_DATABASE=test MYSQL_USER=testuser MYSQL_UNIX_PORT=/var/run/mysqld/mysqld.sock 3 | # 4 | # environments: 5 | # * MYSQL_SERVER - default: 'localhost' 6 | # * MYSQL_USER - default: login user 7 | # * MYSQL_PASSWORD - default: no password 8 | # * MYSQL_DATABASE - default: 'test_for_mysql_ruby' 9 | # * MYSQL_PORT - default: 3306 10 | # * MYSQL_SOCKET - defualt: '/tmp/mysql.sock' 11 | # 12 | 13 | require "bundler/gem_tasks" 14 | 15 | require 'rake/testtask' 16 | Rake::TestTask.new do |t| 17 | t.pattern = 'test/test*.rb' 18 | end 19 | -------------------------------------------------------------------------------- /bench/01_insert.rb: -------------------------------------------------------------------------------- 1 | my = Mysql.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 2 | my.query(<>16].pack("CvC") if num < 16777216 10 | return [254, num&0xffffffff, num>>32].pack("CVV") 11 | end 12 | 13 | # convert String to LengthCodedString 14 | def self.lcs(str) 15 | str = Charset.to_binary str.dup 16 | lcb(str.length)+str 17 | end 18 | 19 | def initialize(data) 20 | @data = data 21 | end 22 | 23 | def lcb 24 | return nil if @data.empty? 25 | case v = utiny 26 | when 0xfb 27 | return nil 28 | when 0xfc 29 | return ushort 30 | when 0xfd 31 | c, v = utiny, ushort 32 | return (v << 8)+c 33 | when 0xfe 34 | v1, v2 = ulong, ulong 35 | return (v2 << 32)+v1 36 | else 37 | return v 38 | end 39 | end 40 | 41 | def lcs 42 | len = self.lcb 43 | return nil unless len 44 | @data.slice!(0, len) 45 | end 46 | 47 | def read(len) 48 | @data.slice!(0, len) 49 | end 50 | 51 | def string 52 | str = @data.unpack('Z*').first 53 | @data.slice!(0, str.length+1) 54 | str 55 | end 56 | 57 | def utiny 58 | @data.slice!(0, 1).unpack('C').first 59 | end 60 | 61 | def ushort 62 | @data.slice!(0, 2).unpack('v').first 63 | end 64 | 65 | def ulong 66 | @data.slice!(0, 4).unpack('V').first 67 | end 68 | 69 | def eof? 70 | @data[0] == ?\xfe && @data.length == 5 71 | end 72 | 73 | def to_s 74 | @data 75 | end 76 | 77 | end 78 | end 79 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [3.0.1] - 2022-06-18 2 | 3 | - LICENSE: correct author 4 | - FIX: correct LOAD DATA LOCAL INFILE result information. 5 | - FIX: reset SERVER_MORE_RESULTS_EXISTS when error packet is received. 6 | - FIX: close the socket when the connection is disconnected. 7 | - FIX: allow multiple results by default. 8 | 9 | ## [3.0.0] - 2021-11-16 10 | 11 | - `Mysql.new` no longer connect. use `Mysql.connect` or `Mysql#connect`. 12 | 13 | - `Mysql.init` is removed. use `Mysql.new` instead. 14 | 15 | - `Mysql.new`, `Mysql.conncet` and `Mysql#connect` takes URI object or URI string or Hash object. 16 | example: 17 | Mysql.connect('mysql://user:password@hostname:port/dbname?charset=ascii') 18 | Mysql.connect('mysql://user:password@%2Ftmp%2Fmysql.sock/dbname?charset=ascii') # for UNIX socket 19 | Mysql.connect('hostname', 'user', 'password', 'dbname') 20 | Mysql.connect(host: 'hostname', username: 'user', password: 'password', database: 'dbname') 21 | 22 | - `Mysql.options` is removed. use `Mysql#param = value` instead. 23 | For example: 24 | m = Mysql.init 25 | m.options(Mysql::OPT_LOCAL_INFILE, true) 26 | m.connect(host, user, passwd) 27 | change to 28 | m = Mysql.new 29 | m.local_infile = true 30 | m.connect(host, user, passwd) 31 | or 32 | m = Mysql.connect(host, user, passwd, local_infile: true) 33 | 34 | - `Mysql::Time` is removed. 35 | Instead, `Time` object is returned for the DATE, DATETIME, TIMESTAMP data, 36 | and `Integer` object is returned for the TIME data. 37 | If DATE, DATETIME, TIMESTAMP are invalid values for Time, nil is returned. 38 | 39 | - meaningless methods are removed: 40 | * `bind_result` 41 | * `client_info` 42 | * `client_version` 43 | * `get_proto_info` 44 | * `get_server_info` 45 | * `get_server_version` 46 | * `proto_info` 47 | * `query_with_result` 48 | 49 | - alias method are removed: 50 | * `get_host_info`: use `host_info` 51 | * `real_connect`: use `connect` 52 | * `real_query`: use `query` 53 | 54 | - methods corresponding to deprecated APIs in MySQL are removed: 55 | * `list_dbs`: use `SHOW DATABASES` 56 | * `list_fields`: use `SHOW COLUMNS` 57 | * `list_processes`: use `SHOW PROCESSLIST` 58 | * `list_tables`: use `SHOW TABLES` 59 | -------------------------------------------------------------------------------- /lib/mysql/authenticator/caching_sha2_password.rb: -------------------------------------------------------------------------------- 1 | require 'digest/sha2' 2 | 3 | class Mysql 4 | class Authenticator 5 | class CachingSha2Password 6 | # @param protocol [Mysql::Protocol] 7 | def initialize(protocol) 8 | @protocol = protocol 9 | end 10 | 11 | # @return [String] 12 | def name 13 | 'caching_sha2_password' 14 | end 15 | 16 | # @param passwd [String] 17 | # @param scramble [String] 18 | # @yield [String] hashed password 19 | # @return [Mysql::Packet] 20 | def authenticate(passwd, scramble) 21 | yield hash_password(passwd, scramble) 22 | pkt = @protocol.read 23 | data = pkt.to_s 24 | if data.size == 2 && data[0] == "\x01" 25 | case data[1] 26 | when "\x03" # fast_auth_success 27 | # OK 28 | when "\x04" # perform_full_authentication 29 | if @protocol.client_flags & CLIENT_SSL != 0 30 | @protocol.write passwd+"\0" 31 | elsif !@protocol.get_server_public_key 32 | raise 'Authentication requires secure connection' 33 | else 34 | @protocol.write "\2" # request public key 35 | pkt = @protocol.read 36 | pkt.utiny # skip 37 | pubkey = pkt.to_s 38 | hash = (passwd+"\0").unpack("C*").zip(scramble.unpack("C*")).map{|a, b| a ^ b}.pack("C*") 39 | enc = OpenSSL::PKey::RSA.new(pubkey).public_encrypt(hash, OpenSSL::PKey::RSA::PKCS1_OAEP_PADDING) 40 | @protocol.write enc 41 | end 42 | else 43 | raise "invalid auth reply packet: #{data.inspect}" 44 | end 45 | pkt = @protocol.read 46 | end 47 | return pkt 48 | end 49 | 50 | # @param passwd [String] 51 | # @param scramble [String] 52 | # @return [String] hashed password 53 | def hash_password(passwd, scramble) 54 | return '' if passwd.nil? or passwd.empty? 55 | hash1 = Digest::SHA256.digest(passwd) 56 | hash2 = Digest::SHA256.digest(hash1) 57 | hash3 = Digest::SHA256.digest(hash2 + scramble) 58 | hash1.unpack("C*").zip(hash3.unpack("C*")).map{|a, b| a ^ b}.pack("C*") 59 | end 60 | end 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /lib/mysql/authenticator.rb: -------------------------------------------------------------------------------- 1 | class Mysql 2 | class Authenticator 3 | @plugins = {} 4 | 5 | # @param plugin [String] 6 | def self.plugin_class(plugin) 7 | return @plugins[plugin] if @plugins[plugin] 8 | 9 | raise ClientError, "invalid plugin name: #{plugin}" unless plugin.match?(/\A\w+\z/) 10 | begin 11 | require_relative "authenticator/#{plugin}" 12 | rescue LoadError 13 | return nil 14 | end 15 | class_name = plugin.gsub(/(?:^|_)(.)/){$1.upcase} 16 | raise ClientError, "#{class_name} is undefined" unless self.const_defined? class_name 17 | klass = self.const_get(class_name) 18 | @plugins[plugin] = klass 19 | return klass 20 | end 21 | 22 | def initialize(protocol) 23 | @protocol = protocol 24 | end 25 | 26 | # @param plugin [String] 27 | def get(plugin) 28 | self.class.plugin_class(plugin) 29 | end 30 | 31 | # @param plugin [String] 32 | def get!(plugin) 33 | get(plugin) or raise ClientError, "unknown plugin: #{plugin}" 34 | end 35 | 36 | def authenticate(user, passwd, db, scramble, plugin_name) 37 | plugin = (get(plugin_name) || DummyPlugin).new(@protocol) 38 | pkt = plugin.authenticate(passwd, scramble) do |hashed| 39 | @protocol.write Protocol::AuthenticationPacket.serialize(@protocol.client_flags, 1024**3, @protocol.charset.number, user, hashed, db, plugin.name) 40 | end 41 | while true 42 | res = Protocol::AuthenticationResultPacket.parse(pkt) 43 | case res.result 44 | when 0 # OK 45 | break 46 | when 2 # multi factor auth 47 | raise ClientError, 'multi factor authentication is not supported' 48 | when 254 # change auth plugin 49 | plugin = get!(res.auth_plugin).new(@protocol) 50 | pkt = plugin.authenticate(passwd, res.scramble) do |hashed| 51 | if passwd.nil? || passwd.empty? 52 | @protocol.write "\0" 53 | else 54 | @protocol.write hashed 55 | end 56 | end 57 | else 58 | raise ClientError, "invalid packet: #{pkt.to_s}" 59 | end 60 | end 61 | end 62 | 63 | class DummyPlugin 64 | # @param protocol [Mysql::Protocol] 65 | def initialize(protocol) 66 | @protocol = protocol 67 | end 68 | 69 | # @return [String] 70 | def name 71 | '' 72 | end 73 | 74 | # @param passwd [String] 75 | # @param scramble [String] 76 | # @yield [String] hashed password 77 | # @return [Mysql::Packet] 78 | def authenticate(passwd, scramble) 79 | yield '' 80 | @protocol.read 81 | end 82 | end 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/test_mysql_packet.rb: -------------------------------------------------------------------------------- 1 | # coding: binary 2 | require 'test/unit' 3 | require 'test/unit/rr' 4 | begin 5 | require 'test/unit/notify' 6 | rescue LoadError 7 | # ignore 8 | end 9 | 10 | require 'mysql' 11 | 12 | class TestMysqlPacket < Test::Unit::TestCase 13 | def self._(s) 14 | s.unpack('H*').first 15 | end 16 | 17 | def subject 18 | Mysql::Packet.new(data) 19 | end 20 | 21 | sub_test_case '#lcb' do 22 | [ 23 | ["\xfb", nil], 24 | ["\xfc\x01\x02", 0x0201], 25 | ["\xfd\x01\x02\x03", 0x030201], 26 | ["\xfe\x01\x02\x03\x04\x05\x06\x07\x08", 0x0807060504030201], 27 | ["\x01", 0x01], 28 | ].each do |data, result| 29 | sub_test_case "for '#{_ data}'" do 30 | define_method(:data){ data } 31 | test '' do 32 | assert{ subject.lcb == result } 33 | end 34 | end 35 | end 36 | end 37 | 38 | sub_test_case '#lcs' do 39 | [ 40 | ["\x03\x41\x42\x43", 'ABC'], 41 | ["\x01", ''], 42 | ["", nil], 43 | ].each do |data, result| 44 | sub_test_case "for '#{_ data}'" do 45 | define_method(:data){ data } 46 | test '' do 47 | assert{ subject.lcs == result } 48 | end 49 | end 50 | end 51 | end 52 | 53 | sub_test_case '#read' do 54 | define_method(:data){'ABCDEFGHI'} 55 | test '' do 56 | assert{ subject.read(7) == 'ABCDEFG' } 57 | end 58 | end 59 | 60 | sub_test_case '#string' do 61 | define_method(:data){"ABC\0DEF"} 62 | test 'should NUL terminated String' do 63 | assert{ subject.string == 'ABC' } 64 | end 65 | end 66 | 67 | sub_test_case '#utiny' do 68 | [ 69 | ["\x01", 0x01], 70 | ["\xFF", 0xff], 71 | ].each do |data, result| 72 | sub_test_case "for '#{_ data}'" do 73 | define_method(:data){data} 74 | test '' do 75 | assert{ subject.utiny == result } 76 | end 77 | end 78 | end 79 | end 80 | 81 | sub_test_case '#ushort' do 82 | [ 83 | ["\x01\x02", 0x0201], 84 | ["\xFF\xFE", 0xfeff], 85 | ].each do |data, result| 86 | sub_test_case "for '#{_ data}'" do 87 | define_method(:data){data} 88 | test '' do 89 | assert{ subject.ushort == result } 90 | end 91 | end 92 | end 93 | end 94 | 95 | sub_test_case '#ulong' do 96 | [ 97 | ["\x01\x02\x03\x04", 0x04030201], 98 | ["\xFF\xFE\xFD\xFC", 0xfcfdfeff], 99 | ].each do |data, result| 100 | sub_test_case "for '#{_ data}'" do 101 | define_method(:data){data} 102 | test '' do 103 | assert{ subject.ulong == result } 104 | end 105 | end 106 | end 107 | end 108 | 109 | sub_test_case '#eof?' do 110 | [ 111 | ["\xfe\x00\x00\x00\x00", true], 112 | ["ABCDE", false], 113 | ].each do |data, result| 114 | sub_test_case "for '#{_ data}'" do 115 | define_method(:data){data} 116 | test '' do 117 | assert{ subject.eof? == result } 118 | end 119 | end 120 | end 121 | end 122 | 123 | sub_test_case 'Mysql::Packet.lcb' do 124 | [ 125 | [nil, "\xfb"], 126 | [1, "\x01"], 127 | [250, "\xfa"], 128 | [251, "\xfc\xfb\x00"], 129 | [65535, "\xfc\xff\xff"], 130 | [65536, "\xfd\x00\x00\x01"], 131 | [16777215, "\xfd\xff\xff\xff"], 132 | [16777216, "\xfe\x00\x00\x00\x01\x00\x00\x00\x00"], 133 | [0xffffffffffffffff, "\xfe\xff\xff\xff\xff\xff\xff\xff\xff"], 134 | ].each do |val, result| 135 | sub_test_case "with #{val}" do 136 | test '' do 137 | assert{ Mysql::Packet.lcb(val) == result } 138 | end 139 | end 140 | end 141 | end 142 | 143 | sub_test_case 'Mysql::Packet.lcs' do 144 | test '' do 145 | assert{ Mysql::Packet.lcs("hoge") == "\x04hoge" } 146 | assert{ Mysql::Packet.lcs("あいう".force_encoding("UTF-8")) == "\x09\xe3\x81\x82\xe3\x81\x84\xe3\x81\x86" } 147 | end 148 | end 149 | end 150 | -------------------------------------------------------------------------------- /lib/mysql/constants.rb: -------------------------------------------------------------------------------- 1 | # coding: ascii-8bit 2 | # Copyright (C) 2003 TOMITA Masahiro 3 | # mailto:tommy@tmtm.org 4 | 5 | class Mysql 6 | # Command 7 | COM_SLEEP = 0 8 | COM_QUIT = 1 9 | COM_INIT_DB = 2 10 | COM_QUERY = 3 11 | COM_FIELD_LIST = 4 12 | COM_CREATE_DB = 5 13 | COM_DROP_DB = 6 14 | COM_REFRESH = 7 15 | COM_SHUTDOWN = 8 16 | COM_STATISTICS = 9 17 | COM_PROCESS_INFO = 10 18 | COM_CONNECT = 11 19 | COM_PROCESS_KILL = 12 20 | COM_DEBUG = 13 21 | COM_PING = 14 22 | COM_TIME = 15 23 | COM_DELAYED_INSERT = 16 24 | COM_CHANGE_USER = 17 25 | COM_BINLOG_DUMP = 18 26 | COM_TABLE_DUMP = 19 27 | COM_CONNECT_OUT = 20 28 | COM_REGISTER_SLAVE = 21 29 | COM_STMT_PREPARE = 22 30 | COM_STMT_EXECUTE = 23 31 | COM_STMT_SEND_LONG_DATA = 24 32 | COM_STMT_CLOSE = 25 33 | COM_STMT_RESET = 26 34 | COM_SET_OPTION = 27 35 | COM_STMT_FETCH = 28 36 | COM_DAEMON = 29 37 | COM_BINLOG_DUMP_GTID = 30 38 | COM_RESET_CONNECTION = 31 39 | COM_CLONE = 32 40 | 41 | # Client flag 42 | CLIENT_LONG_PASSWORD = 1 # new more secure passwords 43 | CLIENT_FOUND_ROWS = 1 << 1 # Found instead of affected rows 44 | CLIENT_LONG_FLAG = 1 << 2 # Get all column flags 45 | CLIENT_CONNECT_WITH_DB = 1 << 3 # One can specify db on connect 46 | CLIENT_NO_SCHEMA = 1 << 4 # Don't allow database.table.column 47 | CLIENT_COMPRESS = 1 << 5 # Can use compression protocol 48 | CLIENT_ODBC = 1 << 6 # Odbc client 49 | CLIENT_LOCAL_FILES = 1 << 7 # Can use LOAD DATA LOCAL 50 | CLIENT_IGNORE_SPACE = 1 << 8 # Ignore spaces before '(' 51 | CLIENT_PROTOCOL_41 = 1 << 9 # New 4.1 protocol 52 | CLIENT_INTERACTIVE = 1 << 10 # This is an interactive client 53 | CLIENT_SSL = 1 << 11 # Switch to SSL after handshake 54 | CLIENT_IGNORE_SIGPIPE = 1 << 12 # IGNORE sigpipes 55 | CLIENT_TRANSACTIONS = 1 << 13 # Client knows about transactions 56 | CLIENT_RESERVED = 1 << 14 # Old flag for 4.1 protocol 57 | CLIENT_SECURE_CONNECTION = 1 << 15 # New 4.1 authentication 58 | CLIENT_MULTI_STATEMENTS = 1 << 16 # Enable/disable multi-stmt support 59 | CLIENT_MULTI_RESULTS = 1 << 17 # Enable/disable multi-results 60 | CLIENT_PS_MULTI_RESULTS = 1 << 18 # Multi-results in PS-protocol 61 | CLIENT_PLUGIN_AUTH = 1 << 19 # Client supports plugin authentication 62 | CLIENT_CONNECT_ATTRS = 1 << 20 # Client supports connection attribute 63 | CLIENT_PLUGIN_AUTH_LENENC_CLIENT_DATA = 1 << 21 # Enable authentication response packet to be larger than 255 bytes. 64 | CLIENT_CAN_HANDLE_EXPIRED_PASSWORDS = 1 << 22 # Don't close the connection for a connection with expired password. 65 | CLIENT_SESSION_TRACK = 1 << 23 # Capable of handling server state change information. Its a hint to the server to include the state change information in Ok packet. 66 | CLIENT_DEPRECATE_EOF = 1 << 24 # Client no longer needs EOF packet 67 | CLIENT_OPTIONAL_RESULTSET_METADATA = 1 << 25 # The client can handle optional metadata information in the resultset. 68 | CLIENT_ZSTD_COMPRESSION_ALGORITHM = 1 << 26 # Compression protocol extended to support zstd compression method 69 | CLIENT_CAPABILITY_EXTENSION = 1 << 29 # This flag will be reserved to extend the 32bit capabilities structure to 64bits. 70 | CLIENT_SSL_VERIFY_SERVER_CERT = 1 << 30 # Verify server certificate. 71 | CLIENT_REMEMBER_OPTIONS = 1 << 31 # Don't reset the options after an unsuccessful connect 72 | 73 | # Connection Option 74 | OPT_CONNECT_TIMEOUT = 0 75 | OPT_COMPRESS = 1 76 | OPT_NAMED_PIPE = 2 77 | INIT_COMMAND = 3 78 | READ_DEFAULT_FILE = 4 79 | READ_DEFAULT_GROUP = 5 80 | SET_CHARSET_DIR = 6 81 | SET_CHARSET_NAME = 7 82 | OPT_LOCAL_INFILE = 8 83 | OPT_PROTOCOL = 9 84 | SHARED_MEMORY_BASE_NAME = 10 85 | OPT_READ_TIMEOUT = 11 86 | OPT_WRITE_TIMEOUT = 12 87 | OPT_USE_RESULT = 13 88 | REPORT_DATA_TRUNCATION = 14 89 | OPT_RECONNECT = 15 90 | PLUGIN_DIR = 16 91 | DEFAULT_AUTH = 17 92 | OPT_BIND = 18 93 | OPT_SSL_KEY = 19 94 | OPT_SSL_CERT = 20 95 | OPT_SSL_CA = 21 96 | OPT_SSL_CAPATH = 22 97 | OPT_SSL_CIPHER = 23 98 | OPT_SSL_CRL = 24 99 | OPT_SSL_CRLPATH = 25 100 | OPT_CONNECT_ATTR_RESET = 26 101 | OPT_CONNECT_ATTR_ADD = 27 102 | OPT_CONNECT_ATTR_DELETE = 28 103 | SERVER_PUBLIC_KEY = 29 104 | ENABLE_CLEARTEXT_PLUGIN = 30 105 | OPT_CAN_HANDLE_EXPIRED_PASSWORDS = 31 106 | OPT_MAX_ALLOWED_PACKET = 32 107 | OPT_NET_BUFFER_LENGTH = 33 108 | OPT_TLS_VERSION = 34 109 | OPT_SSL_MODE = 35 110 | OPT_GET_SERVER_PUBLIC_KEY = 36 111 | OPT_RETRY_COUNT = 37 112 | OPT_OPTIONAL_RESULTSET_METADATA = 38 113 | OPT_SSL_FIPS_MODE = 39 114 | OPT_TLS_CIPHERSUITES = 40 115 | OPT_COMPRESSION_ALGORITHMS = 41 116 | OPT_ZSTD_COMPRESSION_LEVEL = 42 117 | OPT_LOAD_DATA_LOCAL_DIR = 43 118 | 119 | # SSL Mode 120 | SSL_MODE_DISABLED = 1 121 | SSL_MODE_PREFERRED = 2 122 | SSL_MODE_REQUIRED = 3 123 | SSL_MODE_VERIFY_CA = 4 124 | SSL_MODE_VERIFY_IDENTITY = 5 125 | 126 | # Server Option 127 | OPTION_MULTI_STATEMENTS_ON = 0 128 | OPTION_MULTI_STATEMENTS_OFF = 1 129 | 130 | # Server Status 131 | SERVER_STATUS_IN_TRANS = 1 132 | SERVER_STATUS_AUTOCOMMIT = 1 << 1 133 | SERVER_MORE_RESULTS_EXISTS = 1 << 3 134 | SERVER_QUERY_NO_GOOD_INDEX_USED = 1 << 4 135 | SERVER_QUERY_NO_INDEX_USED = 1 << 5 136 | SERVER_STATUS_CURSOR_EXISTS = 1 << 6 137 | SERVER_STATUS_LAST_ROW_SENT = 1 << 7 138 | SERVER_STATUS_DB_DROPPED = 1 << 8 139 | SERVER_STATUS_NO_BACKSLASH_ESCAPES = 1 << 9 140 | SERVER_STATUS_METADATA_CHANGED = 1 << 10 141 | SERVER_QUERY_WAS_SLOW = 1 << 11 142 | SERVER_PS_OUT_PARAMS = 1 << 12 143 | SERVER_STATUS_IN_TRANS_READONLY = 1 << 13 144 | SERVER_SESSION_STATE_CHANGED = 1 << 14 145 | 146 | # Refresh parameter 147 | REFRESH_GRANT = 1 148 | REFRESH_LOG = 1 << 1 149 | REFRESH_TABLES = 1 << 2 150 | REFRESH_HOSTS = 1 << 3 151 | REFRESH_STATUS = 1 << 4 152 | REFRESH_THREADS = 1 << 5 153 | REFRESH_SLAVE = 1 << 6 154 | REFRESH_MASTER = 1 << 7 155 | REFRESH_ERROR_LOG = 1 << 8 156 | REFRESH_ENGINE_LOG = 1 << 9 157 | REFRESH_BINARY_LOG = 1 << 10 158 | REFRESH_RELAY_LOG = 1 << 11 159 | REFRESH_GENERAL_LOG = 1 << 12 160 | REFRESH_SLOW_LOG = 1 << 13 161 | REFRESH_READ_LOCK = 1 << 14 162 | REFRESH_FAST = 1 << 15 163 | REFRESH_QUERY_CACHE = 1 << 16 164 | REFRESH_QUERY_CACHE_FREE = 1 << 17 165 | REFRESH_DES_KEY_FILE = 1 << 18 166 | REFRESH_USER_RESOURCES = 1 << 19 167 | REFRESH_FOR_EXPORT = 1 << 20 168 | REFRESH_OPTIMIZER_COSTS = 1 << 21 169 | REFRESH_PERSIST = 1 << 22 170 | 171 | class Field 172 | # Field type 173 | TYPE_DECIMAL = 0 174 | TYPE_TINY = 1 175 | TYPE_SHORT = 2 176 | TYPE_LONG = 3 177 | TYPE_FLOAT = 4 178 | TYPE_DOUBLE = 5 179 | TYPE_NULL = 6 180 | TYPE_TIMESTAMP = 7 181 | TYPE_LONGLONG = 8 182 | TYPE_INT24 = 9 183 | TYPE_DATE = 10 184 | TYPE_TIME = 11 185 | TYPE_DATETIME = 12 186 | TYPE_YEAR = 13 187 | TYPE_NEWDATE = 14 188 | TYPE_VARCHAR = 15 189 | TYPE_BIT = 16 190 | TYPE_TIMESTAMP2 = 17 191 | TYPE_DATETIME2 = 18 192 | TYPE_TIME2 = 19 193 | TYPE_TYPED_ARRAY = 20 194 | TYPE_INVALID = 243 195 | TYPE_BOOL = 244 196 | TYPE_JSON = 245 197 | TYPE_NEWDECIMAL = 246 198 | TYPE_ENUM = 247 199 | TYPE_SET = 248 200 | TYPE_TINY_BLOB = 249 201 | TYPE_MEDIUM_BLOB = 250 202 | TYPE_LONG_BLOB = 251 203 | TYPE_BLOB = 252 204 | TYPE_VAR_STRING = 253 205 | TYPE_STRING = 254 206 | TYPE_GEOMETRY = 255 207 | TYPE_CHAR = TYPE_TINY 208 | TYPE_INTERVAL = TYPE_ENUM 209 | 210 | # Flag 211 | NOT_NULL_FLAG = 1 212 | PRI_KEY_FLAG = 2 213 | UNIQUE_KEY_FLAG = 4 214 | MULTIPLE_KEY_FLAG = 8 215 | BLOB_FLAG = 16 216 | UNSIGNED_FLAG = 32 217 | ZEROFILL_FLAG = 64 218 | BINARY_FLAG = 128 219 | ENUM_FLAG = 256 220 | AUTO_INCREMENT_FLAG = 512 221 | TIMESTAMP_FLAG = 1024 222 | SET_FLAG = 2048 223 | NO_DEFAULT_VALUE_FLAG = 4096 224 | ON_UPDATE_NOW_FLAG = 8192 225 | NUM_FLAG = 32768 226 | PART_KEY_FLAG = 16384 227 | GROUP_FLAG = 32768 228 | UNIQUE_FLAG = 65536 229 | BINCMP_FLAG = 131072 230 | GET_FIXED_FIELDS_FLAG = 1 << 18 231 | FIELD_IN_PART_FUNC_FLAG = 1 << 19 232 | FIELD_IN_ADD_INDEX = 1 << 20 233 | FIELD_IS_RENAMED = 1 << 21 234 | FIELD_FLAGS_STORAGE_MEDIA_MASK = 3 << 22 235 | FIELD_FLAGS_COLUMN_FORMAT_MASK = 3 << 24 236 | FIELD_IS_DROPPED = 1 << 26 237 | EXPLICIT_NULL_FLAG = 1 << 27 238 | FIELD_IS_MARKED = 1 << 28 239 | NOT_SECONDARY_FLAG = 1 << 29 240 | end 241 | 242 | class Stmt 243 | # Cursor type 244 | CURSOR_TYPE_NO_CURSOR = 0 245 | CURSOR_TYPE_READ_ONLY = 1 246 | CURSOR_TYPE_FOR_UPDATE = 2 247 | CURSOR_TYPE_SCROLLABLE = 4 248 | end 249 | end 250 | -------------------------------------------------------------------------------- /lib/mysql/charset.rb: -------------------------------------------------------------------------------- 1 | # coding: ascii-8bit 2 | # Copyright (C) 2008-2012 TOMITA Masahiro 3 | # mailto:tommy@tmtm.org 4 | 5 | # 6 | class Mysql 7 | # @!attribute [r] number 8 | # @private 9 | # @!attribute [r] name 10 | # @return [String] charset name 11 | # @!attribute [r] csname 12 | # @return [String] collation name 13 | class Charset 14 | # @private 15 | # @param [Integer] number 16 | # @param [String] name 17 | # @param [String] csname 18 | def initialize(number, name, csname) 19 | @number, @name, @csname = number, name, csname 20 | end 21 | 22 | attr_reader :number, :name, :csname 23 | 24 | # [[charset_number, charset_name, collation_name, default], ...] 25 | # @private 26 | CHARSETS = [ 27 | [ 1, "big5", "big5_chinese_ci", true ], 28 | [ 2, "latin2", "latin2_czech_cs", false], 29 | [ 3, "dec8", "dec8_swedish_ci", true ], 30 | [ 4, "cp850", "cp850_general_ci", true ], 31 | [ 5, "latin1", "latin1_german1_ci", false], 32 | [ 6, "hp8", "hp8_english_ci", true ], 33 | [ 7, "koi8r", "koi8r_general_ci", true ], 34 | [ 8, "latin1", "latin1_swedish_ci", true ], 35 | [ 9, "latin2", "latin2_general_ci", true ], 36 | [ 10, "swe7", "swe7_swedish_ci", true ], 37 | [ 11, "ascii", "ascii_general_ci", true ], 38 | [ 12, "ujis", "ujis_japanese_ci", true ], 39 | [ 13, "sjis", "sjis_japanese_ci", true ], 40 | [ 14, "cp1251", "cp1251_bulgarian_ci", false], 41 | [ 15, "latin1", "latin1_danish_ci", false], 42 | [ 16, "hebrew", "hebrew_general_ci", true ], 43 | [ 17, "filename", "filename", true ], 44 | [ 18, "tis620", "tis620_thai_ci", true ], 45 | [ 19, "euckr", "euckr_korean_ci", true ], 46 | [ 20, "latin7", "latin7_estonian_cs", false], 47 | [ 21, "latin2", "latin2_hungarian_ci", false], 48 | [ 22, "koi8u", "koi8u_general_ci", true ], 49 | [ 23, "cp1251", "cp1251_ukrainian_ci", false], 50 | [ 24, "gb2312", "gb2312_chinese_ci", true ], 51 | [ 25, "greek", "greek_general_ci", true ], 52 | [ 26, "cp1250", "cp1250_general_ci", true ], 53 | [ 27, "latin2", "latin2_croatian_ci", false], 54 | [ 28, "gbk", "gbk_chinese_ci", true ], 55 | [ 29, "cp1257", "cp1257_lithuanian_ci", false], 56 | [ 30, "latin5", "latin5_turkish_ci", true ], 57 | [ 31, "latin1", "latin1_german2_ci", false], 58 | [ 32, "armscii8", "armscii8_general_ci", true ], 59 | [ 33, "utf8", "utf8_general_ci", true ], 60 | [ 33, "utf8mb3", "utf8mb3_general_ci", true ], 61 | [ 34, "cp1250", "cp1250_czech_cs", false], 62 | [ 35, "ucs2", "ucs2_general_ci", true ], 63 | [ 36, "cp866", "cp866_general_ci", true ], 64 | [ 37, "keybcs2", "keybcs2_general_ci", true ], 65 | [ 38, "macce", "macce_general_ci", true ], 66 | [ 39, "macroman", "macroman_general_ci", true ], 67 | [ 40, "cp852", "cp852_general_ci", true ], 68 | [ 41, "latin7", "latin7_general_ci", true ], 69 | [ 42, "latin7", "latin7_general_cs", false], 70 | [ 43, "macce", "macce_bin", false], 71 | [ 44, "cp1250", "cp1250_croatian_ci", false], 72 | [ 45, "utf8mb4", "utf8mb4_general_ci", false], 73 | [ 46, "utf8mb4", "utf8mb4_bin", false], 74 | [ 47, "latin1", "latin1_bin", false], 75 | [ 48, "latin1", "latin1_general_ci", false], 76 | [ 49, "latin1", "latin1_general_cs", false], 77 | [ 50, "cp1251", "cp1251_bin", false], 78 | [ 51, "cp1251", "cp1251_general_ci", true ], 79 | [ 52, "cp1251", "cp1251_general_cs", false], 80 | [ 53, "macroman", "macroman_bin", false], 81 | [ 54, "utf16", "utf16_general_ci", true ], 82 | [ 55, "utf16", "utf16_bin", false], 83 | [ 56, "utf16le", "utf16le_general_ci", true ], 84 | [ 57, "cp1256", "cp1256_general_ci", true ], 85 | [ 58, "cp1257", "cp1257_bin", false], 86 | [ 59, "cp1257", "cp1257_general_ci", true ], 87 | [ 60, "utf32", "utf32_general_ci", true ], 88 | [ 61, "utf32", "utf32_bin", false], 89 | [ 62, "utf16le", "utf16le_bin", false], 90 | [ 63, "binary", "binary", true ], 91 | [ 64, "armscii8", "armscii8_bin", false], 92 | [ 65, "ascii", "ascii_bin", false], 93 | [ 66, "cp1250", "cp1250_bin", false], 94 | [ 67, "cp1256", "cp1256_bin", false], 95 | [ 68, "cp866", "cp866_bin", false], 96 | [ 69, "dec8", "dec8_bin", false], 97 | [ 70, "greek", "greek_bin", false], 98 | [ 71, "hebrew", "hebrew_bin", false], 99 | [ 72, "hp8", "hp8_bin", false], 100 | [ 73, "keybcs2", "keybcs2_bin", false], 101 | [ 74, "koi8r", "koi8r_bin", false], 102 | [ 75, "koi8u", "koi8u_bin", false], 103 | [ 76, "utf8", "utf8_tolower_ci", false], 104 | [ 76, "utf8mb3", "utf8mb3_tolower_ci", false], 105 | [ 77, "latin2", "latin2_bin", false], 106 | [ 78, "latin5", "latin5_bin", false], 107 | [ 79, "latin7", "latin7_bin", false], 108 | [ 80, "cp850", "cp850_bin", false], 109 | [ 81, "cp852", "cp852_bin", false], 110 | [ 82, "swe7", "swe7_bin", false], 111 | [ 83, "utf8", "utf8_bin", false], 112 | [ 83, "utf8mb3", "utf8mb3_bin", false], 113 | [ 84, "big5", "big5_bin", false], 114 | [ 85, "euckr", "euckr_bin", false], 115 | [ 86, "gb2312", "gb2312_bin", false], 116 | [ 87, "gbk", "gbk_bin", false], 117 | [ 88, "sjis", "sjis_bin", false], 118 | [ 89, "tis620", "tis620_bin", false], 119 | [ 90, "ucs2", "ucs2_bin", false], 120 | [ 91, "ujis", "ujis_bin", false], 121 | [ 92, "geostd8", "geostd8_general_ci", true ], 122 | [ 93, "geostd8", "geostd8_bin", false], 123 | [ 94, "latin1", "latin1_spanish_ci", false], 124 | [ 95, "cp932", "cp932_japanese_ci", true ], 125 | [ 96, "cp932", "cp932_bin", false], 126 | [ 97, "eucjpms", "eucjpms_japanese_ci", true ], 127 | [ 98, "eucjpms", "eucjpms_bin", false], 128 | [ 99, "cp1250", "cp1250_polish_ci", false], 129 | [101, "utf16", "utf16_unicode_ci", false], 130 | [102, "utf16", "utf16_icelandic_ci", false], 131 | [103, "utf16", "utf16_latvian_ci", false], 132 | [104, "utf16", "utf16_romanian_ci", false], 133 | [105, "utf16", "utf16_slovenian_ci", false], 134 | [106, "utf16", "utf16_polish_ci", false], 135 | [107, "utf16", "utf16_estonian_ci", false], 136 | [108, "utf16", "utf16_spanish_ci", false], 137 | [109, "utf16", "utf16_swedish_ci", false], 138 | [110, "utf16", "utf16_turkish_ci", false], 139 | [111, "utf16", "utf16_czech_ci", false], 140 | [112, "utf16", "utf16_danish_ci", false], 141 | [113, "utf16", "utf16_lithuanian_ci", false], 142 | [114, "utf16", "utf16_slovak_ci", false], 143 | [115, "utf16", "utf16_spanish2_ci", false], 144 | [116, "utf16", "utf16_roman_ci", false], 145 | [117, "utf16", "utf16_persian_ci", false], 146 | [118, "utf16", "utf16_esperanto_ci", false], 147 | [119, "utf16", "utf16_hungarian_ci", false], 148 | [120, "utf16", "utf16_sinhala_ci", false], 149 | [121, "utf16", "utf16_german2_ci", false], 150 | [122, "utf16", "utf16_croatian_ci", false], 151 | [123, "utf16", "utf16_unicode_520_ci", false], 152 | [124, "utf16", "utf16_vietnamese_ci", false], 153 | [128, "ucs2", "ucs2_unicode_ci", false], 154 | [129, "ucs2", "ucs2_icelandic_ci", false], 155 | [130, "ucs2", "ucs2_latvian_ci", false], 156 | [131, "ucs2", "ucs2_romanian_ci", false], 157 | [132, "ucs2", "ucs2_slovenian_ci", false], 158 | [133, "ucs2", "ucs2_polish_ci", false], 159 | [134, "ucs2", "ucs2_estonian_ci", false], 160 | [135, "ucs2", "ucs2_spanish_ci", false], 161 | [136, "ucs2", "ucs2_swedish_ci", false], 162 | [137, "ucs2", "ucs2_turkish_ci", false], 163 | [138, "ucs2", "ucs2_czech_ci", false], 164 | [139, "ucs2", "ucs2_danish_ci", false], 165 | [140, "ucs2", "ucs2_lithuanian_ci", false], 166 | [141, "ucs2", "ucs2_slovak_ci", false], 167 | [142, "ucs2", "ucs2_spanish2_ci", false], 168 | [143, "ucs2", "ucs2_roman_ci", false], 169 | [144, "ucs2", "ucs2_persian_ci", false], 170 | [145, "ucs2", "ucs2_esperanto_ci", false], 171 | [146, "ucs2", "ucs2_hungarian_ci", false], 172 | [147, "ucs2", "ucs2_sinhala_ci", false], 173 | [148, "ucs2", "ucs2_german2_ci", false], 174 | [149, "ucs2", "ucs2_croatian_ci", false], 175 | [150, "ucs2", "ucs2_unicode_520_ci", false], 176 | [151, "ucs2", "ucs2_vietnamese_ci", false], 177 | [159, "ucs2", "ucs2_general_mysql500_ci", false], 178 | [160, "utf32", "utf32_unicode_ci", false], 179 | [161, "utf32", "utf32_icelandic_ci", false], 180 | [162, "utf32", "utf32_latvian_ci", false], 181 | [163, "utf32", "utf32_romanian_ci", false], 182 | [164, "utf32", "utf32_slovenian_ci", false], 183 | [165, "utf32", "utf32_polish_ci", false], 184 | [166, "utf32", "utf32_estonian_ci", false], 185 | [167, "utf32", "utf32_spanish_ci", false], 186 | [168, "utf32", "utf32_swedish_ci", false], 187 | [169, "utf32", "utf32_turkish_ci", false], 188 | [170, "utf32", "utf32_czech_ci", false], 189 | [171, "utf32", "utf32_danish_ci", false], 190 | [172, "utf32", "utf32_lithuanian_ci", false], 191 | [173, "utf32", "utf32_slovak_ci", false], 192 | [174, "utf32", "utf32_spanish2_ci", false], 193 | [175, "utf32", "utf32_roman_ci", false], 194 | [176, "utf32", "utf32_persian_ci", false], 195 | [177, "utf32", "utf32_esperanto_ci", false], 196 | [178, "utf32", "utf32_hungarian_ci", false], 197 | [179, "utf32", "utf32_sinhala_ci", false], 198 | [180, "utf32", "utf32_german2_ci", false], 199 | [181, "utf32", "utf32_croatian_ci", false], 200 | [182, "utf32", "utf32_unicode_520_ci", false], 201 | [183, "utf32", "utf32_vietnamese_ci", false], 202 | [192, "utf8", "utf8_unicode_ci", false], 203 | [193, "utf8", "utf8_icelandic_ci", false], 204 | [194, "utf8", "utf8_latvian_ci", false], 205 | [195, "utf8", "utf8_romanian_ci", false], 206 | [196, "utf8", "utf8_slovenian_ci", false], 207 | [197, "utf8", "utf8_polish_ci", false], 208 | [198, "utf8", "utf8_estonian_ci", false], 209 | [199, "utf8", "utf8_spanish_ci", false], 210 | [200, "utf8", "utf8_swedish_ci", false], 211 | [201, "utf8", "utf8_turkish_ci", false], 212 | [202, "utf8", "utf8_czech_ci", false], 213 | [203, "utf8", "utf8_danish_ci", false], 214 | [204, "utf8", "utf8_lithuanian_ci", false], 215 | [205, "utf8", "utf8_slovak_ci", false], 216 | [206, "utf8", "utf8_spanish2_ci", false], 217 | [207, "utf8", "utf8_roman_ci", false], 218 | [208, "utf8", "utf8_persian_ci", false], 219 | [209, "utf8", "utf8_esperanto_ci", false], 220 | [210, "utf8", "utf8_hungarian_ci", false], 221 | [211, "utf8", "utf8_sinhala_ci", false], 222 | [212, "utf8", "utf8_german2_ci", false], 223 | [213, "utf8", "utf8_croatian_ci", false], 224 | [214, "utf8", "utf8_unicode_520_ci", false], 225 | [215, "utf8", "utf8_vietnamese_ci", false], 226 | [223, "utf8", "utf8_general_mysql500_ci", false], 227 | [192, "utf8mb3", "utf8mb3_unicode_ci", false], 228 | [193, "utf8mb3", "utf8mb3_icelandic_ci", false], 229 | [194, "utf8mb3", "utf8mb3_latvian_ci", false], 230 | [195, "utf8mb3", "utf8mb3_romanian_ci", false], 231 | [196, "utf8mb3", "utf8mb3_slovenian_ci", false], 232 | [197, "utf8mb3", "utf8mb3_polish_ci", false], 233 | [198, "utf8mb3", "utf8mb3_estonian_ci", false], 234 | [199, "utf8mb3", "utf8mb3_spanish_ci", false], 235 | [200, "utf8mb3", "utf8mb3_swedish_ci", false], 236 | [201, "utf8mb3", "utf8mb3_turkish_ci", false], 237 | [202, "utf8mb3", "utf8mb3_czech_ci", false], 238 | [203, "utf8mb3", "utf8mb3_danish_ci", false], 239 | [204, "utf8mb3", "utf8mb3_lithuanian_ci", false], 240 | [205, "utf8mb3", "utf8mb3_slovak_ci", false], 241 | [206, "utf8mb3", "utf8mb3_spanish2_ci", false], 242 | [207, "utf8mb3", "utf8mb3_roman_ci", false], 243 | [208, "utf8mb3", "utf8mb3_persian_ci", false], 244 | [209, "utf8mb3", "utf8mb3_esperanto_ci", false], 245 | [210, "utf8mb3", "utf8mb3_hungarian_ci", false], 246 | [211, "utf8mb3", "utf8mb3_sinhala_ci", false], 247 | [212, "utf8mb3", "utf8mb3_german2_ci", false], 248 | [213, "utf8mb3", "utf8mb3_croatian_ci", false], 249 | [214, "utf8mb3", "utf8mb3_unicode_520_ci", false], 250 | [215, "utf8mb3", "utf8mb3_vietnamese_ci", false], 251 | [223, "utf8mb3", "utf8mb3_general_mysql500_ci", false], 252 | [224, "utf8mb4", "utf8mb4_unicode_ci", false], 253 | [225, "utf8mb4", "utf8mb4_icelandic_ci", false], 254 | [226, "utf8mb4", "utf8mb4_latvian_ci", false], 255 | [227, "utf8mb4", "utf8mb4_romanian_ci", false], 256 | [228, "utf8mb4", "utf8mb4_slovenian_ci", false], 257 | [229, "utf8mb4", "utf8mb4_polish_ci", false], 258 | [230, "utf8mb4", "utf8mb4_estonian_ci", false], 259 | [231, "utf8mb4", "utf8mb4_spanish_ci", false], 260 | [232, "utf8mb4", "utf8mb4_swedish_ci", false], 261 | [233, "utf8mb4", "utf8mb4_turkish_ci", false], 262 | [234, "utf8mb4", "utf8mb4_czech_ci", false], 263 | [235, "utf8mb4", "utf8mb4_danish_ci", false], 264 | [236, "utf8mb4", "utf8mb4_lithuanian_ci", false], 265 | [237, "utf8mb4", "utf8mb4_slovak_ci", false], 266 | [238, "utf8mb4", "utf8mb4_spanish2_ci", false], 267 | [239, "utf8mb4", "utf8mb4_roman_ci", false], 268 | [240, "utf8mb4", "utf8mb4_persian_ci", false], 269 | [241, "utf8mb4", "utf8mb4_esperanto_ci", false], 270 | [242, "utf8mb4", "utf8mb4_hungarian_ci", false], 271 | [243, "utf8mb4", "utf8mb4_sinhala_ci", false], 272 | [244, "utf8mb4", "utf8mb4_german2_ci", false], 273 | [245, "utf8mb4", "utf8mb4_croatian_ci", false], 274 | [246, "utf8mb4", "utf8mb4_unicode_520_ci", false], 275 | [247, "utf8mb4", "utf8mb4_vietnamese_ci", false], 276 | [248, "gb18030", "gb18030_chinese_ci", true ], 277 | [249, "gb18030", "gb18030_bin", false], 278 | [250, "gb18030", "gb18030_unicode_520_ci", false], 279 | [254, "utf8", "utf8mb3_general_cs", false], 280 | [254, "utf8mb3", "utf8mb3_general_cs", false], 281 | [255, "utf8mb4", "utf8mb4_0900_ai_ci", true ], 282 | [256, "utf8mb4", "utf8mb4_de_pb_0900_ai_ci", false], 283 | [257, "utf8mb4", "utf8mb4_is_0900_ai_ci", false], 284 | [258, "utf8mb4", "utf8mb4_lv_0900_ai_ci", false], 285 | [259, "utf8mb4", "utf8mb4_ro_0900_ai_ci", false], 286 | [260, "utf8mb4", "utf8mb4_sl_0900_ai_ci", false], 287 | [261, "utf8mb4", "utf8mb4_pl_0900_ai_ci", false], 288 | [262, "utf8mb4", "utf8mb4_et_0900_ai_ci", false], 289 | [263, "utf8mb4", "utf8mb4_es_0900_ai_ci", false], 290 | [264, "utf8mb4", "utf8mb4_sv_0900_ai_ci", false], 291 | [265, "utf8mb4", "utf8mb4_tr_0900_ai_ci", false], 292 | [266, "utf8mb4", "utf8mb4_cs_0900_ai_ci", false], 293 | [267, "utf8mb4", "utf8mb4_da_0900_ai_ci", false], 294 | [268, "utf8mb4", "utf8mb4_lt_0900_ai_ci", false], 295 | [269, "utf8mb4", "utf8mb4_sk_0900_ai_ci", false], 296 | [270, "utf8mb4", "utf8mb4_es_trad_0900_ai_ci", false], 297 | [271, "utf8mb4", "utf8mb4_la_0900_ai_ci", false], 298 | [273, "utf8mb4", "utf8mb4_eo_0900_ai_ci", false], 299 | [274, "utf8mb4", "utf8mb4_hu_0900_ai_ci", false], 300 | [275, "utf8mb4", "utf8mb4_hr_0900_ai_ci", false], 301 | [277, "utf8mb4", "utf8mb4_vi_0900_ai_ci", false], 302 | [278, "utf8mb4", "utf8mb4_0900_as_cs", false], 303 | [279, "utf8mb4", "utf8mb4_de_pb_0900_as_cs", false], 304 | [280, "utf8mb4", "utf8mb4_is_0900_as_cs", false], 305 | [281, "utf8mb4", "utf8mb4_lv_0900_as_cs", false], 306 | [282, "utf8mb4", "utf8mb4_ro_0900_as_cs", false], 307 | [283, "utf8mb4", "utf8mb4_sl_0900_as_cs", false], 308 | [284, "utf8mb4", "utf8mb4_pl_0900_as_cs", false], 309 | [285, "utf8mb4", "utf8mb4_et_0900_as_cs", false], 310 | [286, "utf8mb4", "utf8mb4_es_0900_as_cs", false], 311 | [287, "utf8mb4", "utf8mb4_sv_0900_as_cs", false], 312 | [288, "utf8mb4", "utf8mb4_tr_0900_as_cs", false], 313 | [289, "utf8mb4", "utf8mb4_cs_0900_as_cs", false], 314 | [290, "utf8mb4", "utf8mb4_da_0900_as_cs", false], 315 | [291, "utf8mb4", "utf8mb4_lt_0900_as_cs", false], 316 | [292, "utf8mb4", "utf8mb4_sk_0900_as_cs", false], 317 | [293, "utf8mb4", "utf8mb4_es_trad_0900_as_cs", false], 318 | [294, "utf8mb4", "utf8mb4_la_0900_as_cs", false], 319 | [296, "utf8mb4", "utf8mb4_eo_0900_as_cs", false], 320 | [297, "utf8mb4", "utf8mb4_hu_0900_as_cs", false], 321 | [298, "utf8mb4", "utf8mb4_hr_0900_as_cs", false], 322 | [300, "utf8mb4", "utf8mb4_vi_0900_as_cs", false], 323 | [303, "utf8mb4", "utf8mb4_ja_0900_as_cs", false], 324 | [304, "utf8mb4", "utf8mb4_ja_0900_as_cs_ks", false], 325 | [305, "utf8mb4", "utf8mb4_0900_as_ci", false], 326 | [306, "utf8mb4", "utf8mb4_ru_0900_ai_ci", false], 327 | [307, "utf8mb4", "utf8mb4_ru_0900_as_cs", false], 328 | [308, "utf8mb4", "utf8mb4_zh_0900_as_cs", false], 329 | [309, "utf8mb4", "utf8mb4_0900_bin", false], 330 | ] 331 | 332 | # @private 333 | NUMBER_TO_CHARSET = {} 334 | # @private 335 | COLLATION_TO_CHARSET = {} 336 | # @private 337 | CHARSET_DEFAULT = {} 338 | CHARSETS.each do |number, csname, clname, default| 339 | cs = Charset.new number, csname, clname 340 | NUMBER_TO_CHARSET[number] = cs 341 | COLLATION_TO_CHARSET[clname] = cs 342 | CHARSET_DEFAULT[csname] = cs if default 343 | end 344 | 345 | # @private 346 | BINARY_CHARSET_NUMBER = CHARSET_DEFAULT['binary'].number 347 | 348 | # @private 349 | # @param [Integer] n 350 | # @return [Mysql::Charset] 351 | def self.by_number(n) 352 | raise ClientError, "unknown charset number: #{n}" unless NUMBER_TO_CHARSET.key? n 353 | NUMBER_TO_CHARSET[n] 354 | end 355 | 356 | # @private 357 | # @param [String] str 358 | # @return [Mysql::Charset] 359 | def self.by_name(str) 360 | ret = COLLATION_TO_CHARSET[str] || CHARSET_DEFAULT[str] 361 | raise ClientError, "unknown charset: #{str}" unless ret 362 | ret 363 | end 364 | 365 | # @private 366 | # MySQL Charset -> Ruby's Encoding 367 | CHARSET_ENCODING = { 368 | "armscii8" => nil, 369 | "ascii" => Encoding::US_ASCII, 370 | "big5" => Encoding::Big5, 371 | "binary" => Encoding::ASCII_8BIT, 372 | "cp1250" => Encoding::Windows_1250, 373 | "cp1251" => Encoding::Windows_1251, 374 | "cp1256" => Encoding::Windows_1256, 375 | "cp1257" => Encoding::Windows_1257, 376 | "cp850" => Encoding::CP850, 377 | "cp852" => Encoding::CP852, 378 | "cp866" => Encoding::IBM866, 379 | "cp932" => Encoding::Windows_31J, 380 | "dec8" => nil, 381 | "eucjpms" => Encoding::EucJP_ms, 382 | "euckr" => Encoding::EUC_KR, 383 | "gb18030" => Encoding::GB18030, 384 | "gb2312" => Encoding::EUC_CN, 385 | "gbk" => Encoding::GBK, 386 | "geostd8" => nil, 387 | "greek" => Encoding::ISO_8859_7, 388 | "hebrew" => Encoding::ISO_8859_8, 389 | "hp8" => nil, 390 | "keybcs2" => nil, 391 | "koi8r" => Encoding::KOI8_R, 392 | "koi8u" => Encoding::KOI8_U, 393 | "latin1" => Encoding::ISO_8859_1, 394 | "latin2" => Encoding::ISO_8859_2, 395 | "latin5" => Encoding::ISO_8859_9, 396 | "latin7" => Encoding::ISO_8859_13, 397 | "macce" => Encoding::MacCentEuro, 398 | "macroman" => Encoding::MacRoman, 399 | "sjis" => Encoding::SHIFT_JIS, 400 | "swe7" => nil, 401 | "tis620" => Encoding::TIS_620, 402 | "ucs2" => Encoding::UTF_16BE, 403 | "ujis" => Encoding::EucJP_ms, 404 | "utf16" => Encoding::UTF_16BE, 405 | "utf16le" => Encoding::UTF_16LE, 406 | "utf32" => Encoding::UTF_32BE, 407 | "utf8" => Encoding::UTF_8, 408 | "utf8mb3" => Encoding::UTF_8, 409 | "utf8mb4" => Encoding::UTF_8, 410 | } 411 | 412 | # @private 413 | # @param [String] value 414 | # @return [String] 415 | def self.to_binary(value) 416 | value.force_encoding Encoding::ASCII_8BIT 417 | end 418 | 419 | # @private 420 | # convert raw to encoding and convert to Encoding.default_internal 421 | # @param [String] raw 422 | # @param [Encoding] encoding 423 | # @return [String] result 424 | def self.convert_encoding(raw, encoding) 425 | raw.force_encoding(encoding).encode 426 | end 427 | 428 | # @private 429 | # retrun corresponding Ruby encoding 430 | # @return [Encoding] encoding 431 | def encoding 432 | enc = CHARSET_ENCODING[@name.downcase] 433 | raise Mysql::ClientError, "unsupported charset: #{@name}" unless enc 434 | enc 435 | end 436 | 437 | # @private 438 | # convert encoding to corrensponding to MySQL charset 439 | # @param [String] value 440 | # @return [String] 441 | def convert(value) 442 | if value.is_a? String and value.encoding != Encoding::ASCII_8BIT 443 | value = value.encode encoding 444 | end 445 | value 446 | end 447 | end 448 | end 449 | -------------------------------------------------------------------------------- /lib/mysql/protocol.rb: -------------------------------------------------------------------------------- 1 | # coding: ascii-8bit 2 | # Copyright (C) 2008 TOMITA Masahiro 3 | # mailto:tommy@tmtm.org 4 | 5 | require "socket" 6 | require "stringio" 7 | require "openssl" 8 | require_relative 'authenticator.rb' 9 | 10 | class Mysql 11 | # MySQL network protocol 12 | class Protocol 13 | 14 | VERSION = 10 15 | MAX_PACKET_LENGTH = 2**24-1 16 | 17 | # Convert netdata to Ruby value 18 | # @param data [Packet] packet data 19 | # @param type [Integer] field type 20 | # @param unsigned [true or false] true if value is unsigned 21 | # @return [Object] converted value. 22 | def self.net2value(pkt, type, unsigned) 23 | case type 24 | when Field::TYPE_STRING, Field::TYPE_VAR_STRING, Field::TYPE_NEWDECIMAL, Field::TYPE_BLOB, Field::TYPE_JSON 25 | return pkt.lcs 26 | when Field::TYPE_TINY 27 | v = pkt.utiny 28 | return unsigned ? v : v < 128 ? v : v-256 29 | when Field::TYPE_SHORT 30 | v = pkt.ushort 31 | return unsigned ? v : v < 32768 ? v : v-65536 32 | when Field::TYPE_INT24, Field::TYPE_LONG 33 | v = pkt.ulong 34 | return unsigned ? v : v < 0x8000_0000 ? v : v-0x10000_0000 35 | when Field::TYPE_LONGLONG 36 | n1, n2 = pkt.ulong, pkt.ulong 37 | v = (n2 << 32) | n1 38 | return unsigned ? v : v < 0x8000_0000_0000_0000 ? v : v-0x10000_0000_0000_0000 39 | when Field::TYPE_FLOAT 40 | return pkt.read(4).unpack('e').first 41 | when Field::TYPE_DOUBLE 42 | return pkt.read(8).unpack('E').first 43 | when Field::TYPE_DATE 44 | len = pkt.utiny 45 | y, m, d = pkt.read(len).unpack("vCC") 46 | t = Time.new(y, m, d) rescue nil 47 | return t 48 | when Field::TYPE_DATETIME, Field::TYPE_TIMESTAMP 49 | len = pkt.utiny 50 | y, m, d, h, mi, s, sp = pkt.read(len).unpack("vCCCCCV") 51 | return Time.new(y, m, d, h, mi, Rational((s.to_i*1000000+sp.to_i)/1000000)) rescue nil 52 | when Field::TYPE_TIME 53 | len = pkt.utiny 54 | sign, d, h, mi, s, sp = pkt.read(len).unpack("CVCCCV") 55 | r = d.to_i*86400 + h.to_i*3600 + mi.to_i*60 + s.to_i + sp.to_f/1000000 56 | r *= -1 if sign != 0 57 | return r 58 | when Field::TYPE_YEAR 59 | return pkt.ushort 60 | when Field::TYPE_BIT 61 | return pkt.lcs 62 | else 63 | raise "not implemented: type=#{type}" 64 | end 65 | end 66 | 67 | # convert Ruby value to netdata 68 | # @param v [Object] Ruby value. 69 | # @return [Integer] type of column. Field::TYPE_* 70 | # @return [String] netdata 71 | # @raise [ProtocolError] value too large / value is not supported 72 | def self.value2net(v) 73 | case v 74 | when nil 75 | type = Field::TYPE_NULL 76 | val = "" 77 | when Integer 78 | if -0x8000_0000 <= v && v < 0x8000_0000 79 | type = Field::TYPE_LONG 80 | val = [v].pack('V') 81 | elsif -0x8000_0000_0000_0000 <= v && v < 0x8000_0000_0000_0000 82 | type = Field::TYPE_LONGLONG 83 | val = [v&0xffffffff, v>>32].pack("VV") 84 | elsif 0x8000_0000_0000_0000 <= v && v <= 0xffff_ffff_ffff_ffff 85 | type = Field::TYPE_LONGLONG | 0x8000 86 | val = [v&0xffffffff, v>>32].pack("VV") 87 | else 88 | raise ProtocolError, "value too large: #{v}" 89 | end 90 | when Float 91 | type = Field::TYPE_DOUBLE 92 | val = [v].pack("E") 93 | when String 94 | type = Field::TYPE_STRING 95 | val = Packet.lcs(v) 96 | when Time 97 | type = Field::TYPE_DATETIME 98 | val = [11, v.year, v.month, v.day, v.hour, v.min, v.sec, v.usec].pack("CvCCCCCV") 99 | else 100 | raise ProtocolError, "class #{v.class} is not supported" 101 | end 102 | return type, val 103 | end 104 | 105 | attr_reader :server_info 106 | attr_reader :server_version 107 | attr_reader :thread_id 108 | attr_reader :client_flags 109 | attr_reader :sqlstate 110 | attr_reader :affected_rows 111 | attr_reader :insert_id 112 | attr_reader :server_status 113 | attr_reader :warning_count 114 | attr_reader :message 115 | attr_reader :get_server_public_key 116 | attr_accessor :charset 117 | 118 | # @state variable keep state for connection. 119 | # :INIT :: Initial state. 120 | # :READY :: Ready for command. 121 | # :FIELD :: After query(). retr_fields() is needed. 122 | # :RESULT :: After retr_fields(), retr_all_records() or stmt_retr_all_records() is needed. 123 | 124 | # make socket connection to server. 125 | # @param opts [Hash] 126 | # @option :host [String] hostname mysqld running 127 | # @option :username [String] username to connect to mysqld 128 | # @option :password [String] password to connect to mysqld 129 | # @option :database [String] initial database name 130 | # @option :port [String] port number (used if host is not 'localhost' or nil) 131 | # @option :socket [String] socket filename (used if host is 'localhost' or nil) 132 | # @option :flags [Integer] connection flag. Mysql::CLIENT_* ORed 133 | # @option :charset [Mysql::Charset] character set 134 | # @option :connect_timeout [Numeric, nil] 135 | # @option :read_timeout [Numeric, nil] 136 | # @option :write_timeout [Numeric, nil] 137 | # @option :local_infile [Boolean] 138 | # @option :load_data_local_dir [String] 139 | # @option :ssl_mode [Integer] 140 | # @option :get_server_public_key [Boolean] 141 | # @raise [ClientError] connection timeout 142 | def initialize(opts) 143 | @opts = opts 144 | @charset = Mysql::Charset.by_name("utf8mb4") 145 | @insert_id = 0 146 | @warning_count = 0 147 | @gc_stmt_queue = [] # stmt id list which GC destroy. 148 | set_state :INIT 149 | @get_server_public_key = @opts[:get_server_public_key] 150 | begin 151 | if @opts[:host].nil? or @opts[:host].empty? or @opts[:host] == "localhost" 152 | socket = @opts[:socket] || ENV["MYSQL_UNIX_PORT"] || MYSQL_UNIX_PORT 153 | @socket = Socket.unix(socket) 154 | else 155 | port = @opts[:port] || ENV["MYSQL_TCP_PORT"] || (Socket.getservbyname("mysql","tcp") rescue MYSQL_TCP_PORT) 156 | @socket = Socket.tcp(@opts[:host], port, connect_timeout: @opts[:connect_timeout]) 157 | end 158 | rescue Errno::ETIMEDOUT 159 | raise ClientError, "connection timeout" 160 | end 161 | end 162 | 163 | def close 164 | @socket.close 165 | end 166 | 167 | # initial negotiate and authenticate. 168 | # @param charset [Mysql::Charset, nil] charset for connection. nil: use server's charset 169 | # @raise [ProtocolError] The old style password is not supported 170 | def authenticate 171 | check_state :INIT 172 | reset 173 | init_packet = InitialPacket.parse read 174 | @server_info = init_packet.server_version 175 | @server_version = init_packet.server_version.split(/\D/)[0,3].inject{|a,b|a.to_i*100+b.to_i} 176 | @server_capabilities = init_packet.server_capabilities 177 | @thread_id = init_packet.thread_id 178 | @client_flags = CLIENT_LONG_PASSWORD | CLIENT_LONG_FLAG | CLIENT_TRANSACTIONS | CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION | CLIENT_MULTI_RESULTS | CLIENT_PS_MULTI_RESULTS | CLIENT_PLUGIN_AUTH 179 | @client_flags |= CLIENT_LOCAL_FILES if @opts[:local_infile] || @opts[:load_data_local_dir] 180 | @client_flags |= CLIENT_CONNECT_WITH_DB if @opts[:database] 181 | @client_flags |= @opts[:flags] 182 | if @opts[:charset] 183 | @charset = @opts[:charset].is_a?(Charset) ? @opts[:charset] : Charset.by_name(@opts[:charset]) 184 | else 185 | @charset = Charset.by_number(init_packet.server_charset) 186 | @charset.encoding # raise error if unsupported charset 187 | end 188 | enable_ssl 189 | Authenticator.new(self).authenticate(@opts[:username], @opts[:password].to_s, @opts[:database], init_packet.scramble_buff, init_packet.auth_plugin) 190 | set_state :READY 191 | end 192 | 193 | def enable_ssl 194 | case @opts[:ssl_mode] 195 | when SSL_MODE_DISABLED, '1', 'disabled' 196 | return 197 | when SSL_MODE_PREFERRED, '2', 'preferred' 198 | return if @socket.local_address.unix? 199 | return if @server_capabilities & CLIENT_SSL == 0 200 | when SSL_MODE_REQUIRED, '3', 'required' 201 | if @server_capabilities & CLIENT_SSL == 0 202 | raise ClientError::SslConnectionError, "SSL is required but the server doesn't support it" 203 | end 204 | else 205 | raise ClientError, "ssl_mode #{@opts[:ssl_mode]} is not supported" 206 | end 207 | begin 208 | @client_flags |= CLIENT_SSL 209 | write Protocol::TlsAuthenticationPacket.serialize(@client_flags, 1024**3, @charset.number) 210 | @socket = OpenSSL::SSL::SSLSocket.new(@socket) 211 | @socket.sync_close = true 212 | @socket.connect 213 | rescue => e 214 | @client_flags &= ~CLIENT_SSL 215 | return if @opts[:ssl_mode] == SSL_MODE_PREFERRED 216 | raise e 217 | end 218 | end 219 | 220 | # Quit command 221 | def quit_command 222 | synchronize do 223 | reset 224 | write [COM_QUIT].pack("C") 225 | close 226 | end 227 | end 228 | 229 | # Query command 230 | # @param query [String] query string 231 | # @return [Integer, nil] number of fields of results. nil if no results. 232 | def query_command(query) 233 | check_state :READY 234 | begin 235 | reset 236 | write [COM_QUERY, @charset.convert(query)].pack("Ca*") 237 | get_result 238 | rescue 239 | set_state :READY 240 | raise 241 | end 242 | end 243 | 244 | # get result of query. 245 | # @return [integer, nil] number of fields of results. nil if no results. 246 | def get_result 247 | begin 248 | res_packet = ResultPacket.parse read 249 | if res_packet.field_count.to_i > 0 # result data exists 250 | set_state :FIELD 251 | return res_packet.field_count 252 | end 253 | if res_packet.field_count.nil? # LOAD DATA LOCAL INFILE 254 | send_local_file(res_packet.message) 255 | res_packet = ResultPacket.parse read 256 | end 257 | @affected_rows, @insert_id, @server_status, @warning_count, @message = 258 | res_packet.affected_rows, res_packet.insert_id, res_packet.server_status, res_packet.warning_count, res_packet.message 259 | set_state :READY 260 | return nil 261 | rescue 262 | set_state :READY 263 | raise 264 | end 265 | end 266 | 267 | # send local file to server 268 | def send_local_file(filename) 269 | filename = File.absolute_path(filename) 270 | if @opts[:local_infile] || @opts[:load_data_local_dir] && filename.start_with?(@opts[:load_data_local_dir]) 271 | File.open(filename){|f| write f} 272 | else 273 | raise ClientError::LoadDataLocalInfileRejected, 'LOAD DATA LOCAL INFILE file request rejected due to restrictions on access.' 274 | end 275 | ensure 276 | write nil # EOF mark 277 | end 278 | 279 | # Retrieve n fields 280 | # @param n [Integer] number of fields 281 | # @return [Array] field list 282 | def retr_fields(n) 283 | check_state :FIELD 284 | begin 285 | fields = n.times.map{Field.new FieldPacket.parse(read)} 286 | read_eof_packet 287 | set_state :RESULT 288 | fields 289 | rescue 290 | set_state :READY 291 | raise 292 | end 293 | end 294 | 295 | # Retrieve all records for simple query 296 | # @param fields [Array] number of fields 297 | # @return [Array>] all records 298 | def retr_all_records(fields) 299 | check_state :RESULT 300 | enc = charset.encoding 301 | begin 302 | all_recs = [] 303 | until (pkt = read).eof? 304 | all_recs.push RawRecord.new(pkt, fields, enc) 305 | end 306 | pkt.read(3) 307 | @server_status = pkt.utiny 308 | all_recs 309 | ensure 310 | set_state :READY 311 | end 312 | end 313 | 314 | # Ping command 315 | def ping_command 316 | simple_command [COM_PING].pack("C") 317 | end 318 | 319 | # Kill command 320 | def kill_command(pid) 321 | simple_command [COM_PROCESS_KILL, pid].pack("CV") 322 | end 323 | 324 | # Refresh command 325 | def refresh_command(op) 326 | simple_command [COM_REFRESH, op].pack("CC") 327 | end 328 | 329 | # Set option command 330 | def set_option_command(opt) 331 | simple_command [COM_SET_OPTION, opt].pack("Cv") 332 | end 333 | 334 | # Shutdown command 335 | def shutdown_command(level) 336 | simple_command [COM_SHUTDOWN, level].pack("CC") 337 | end 338 | 339 | # Statistics command 340 | def statistics_command 341 | simple_command [COM_STATISTICS].pack("C") 342 | end 343 | 344 | # Stmt prepare command 345 | # @param stmt [String] prepared statement 346 | # @return [Array>] statement id, number of parameters, field list 347 | def stmt_prepare_command(stmt) 348 | synchronize do 349 | reset 350 | write [COM_STMT_PREPARE, charset.convert(stmt)].pack("Ca*") 351 | res_packet = PrepareResultPacket.parse read 352 | if res_packet.param_count > 0 353 | res_packet.param_count.times{read} # skip parameter packet 354 | read_eof_packet 355 | end 356 | if res_packet.field_count > 0 357 | fields = res_packet.field_count.times.map{Field.new FieldPacket.parse(read)} 358 | read_eof_packet 359 | else 360 | fields = [] 361 | end 362 | return res_packet.statement_id, res_packet.param_count, fields 363 | end 364 | end 365 | 366 | # Stmt execute command 367 | # @param stmt_id [Integer] statement id 368 | # @param values [Array] parameters 369 | # @return [Integer] number of fields 370 | def stmt_execute_command(stmt_id, values) 371 | check_state :READY 372 | begin 373 | reset 374 | write ExecutePacket.serialize(stmt_id, Mysql::Stmt::CURSOR_TYPE_NO_CURSOR, values) 375 | get_result 376 | rescue 377 | set_state :READY 378 | raise 379 | end 380 | end 381 | 382 | # Retrieve all records for prepared statement 383 | # @param fields [Array of Mysql::Fields] field list 384 | # @param charset [Mysql::Charset] 385 | # @return [Array>] all records 386 | def stmt_retr_all_records(fields, charset) 387 | check_state :RESULT 388 | enc = charset.encoding 389 | begin 390 | all_recs = [] 391 | until (pkt = read).eof? 392 | all_recs.push StmtRawRecord.new(pkt, fields, enc) 393 | end 394 | all_recs 395 | ensure 396 | set_state :READY 397 | end 398 | end 399 | 400 | # Stmt close command 401 | # @param stmt_id [Integer] statement id 402 | def stmt_close_command(stmt_id) 403 | synchronize do 404 | reset 405 | write [COM_STMT_CLOSE, stmt_id].pack("CV") 406 | end 407 | end 408 | 409 | def gc_stmt(stmt_id) 410 | @gc_stmt_queue.push stmt_id 411 | end 412 | 413 | def check_state(st) 414 | raise 'command out of sync' unless @state == st 415 | end 416 | 417 | def set_state(st) 418 | @state = st 419 | if st == :READY && !@gc_stmt_queue.empty? 420 | gc_disabled = GC.disable 421 | begin 422 | while st = @gc_stmt_queue.shift 423 | reset 424 | write [COM_STMT_CLOSE, st].pack("CV") 425 | end 426 | ensure 427 | GC.enable unless gc_disabled 428 | end 429 | end 430 | end 431 | 432 | def synchronize 433 | begin 434 | check_state :READY 435 | return yield 436 | ensure 437 | set_state :READY 438 | end 439 | end 440 | 441 | # Reset sequence number 442 | def reset 443 | @seq = 0 # packet counter. reset by each command 444 | end 445 | 446 | # Read one packet data 447 | # @return [Packet] packet data 448 | # @rails [ProtocolError] invalid packet sequence number 449 | def read 450 | data = '' 451 | len = nil 452 | begin 453 | header = read_timeout(4, @opts[:read_timeout]) 454 | raise EOFError unless header && header.length == 4 455 | len1, len2, seq = header.unpack("CvC") 456 | len = (len2 << 8) + len1 457 | raise ProtocolError, "invalid packet: sequence number mismatch(#{seq} != #{@seq}(expected))" if @seq != seq 458 | @seq = (@seq + 1) % 256 459 | ret = read_timeout(len, @opts[:read_timeout]) 460 | raise EOFError unless ret && ret.length == len 461 | data.concat ret 462 | rescue EOFError 463 | @socket.close rescue nil 464 | raise ClientError::ServerGoneError, 'MySQL server has gone away' 465 | rescue Errno::ETIMEDOUT 466 | raise ClientError, "read timeout" 467 | end while len == MAX_PACKET_LENGTH 468 | 469 | @sqlstate = "00000" 470 | 471 | # Error packet 472 | if data[0] == ?\xff 473 | _, errno, marker, @sqlstate, message = data.unpack("Cvaa5a*") 474 | unless marker == "#" 475 | _, errno, message = data.unpack("Cva*") # Version 4.0 Error 476 | @sqlstate = "" 477 | end 478 | @server_status &= ~SERVER_MORE_RESULTS_EXISTS 479 | message.force_encoding(@charset.encoding) 480 | if Mysql::ServerError::ERROR_MAP.key? errno 481 | raise Mysql::ServerError::ERROR_MAP[errno].new(message, @sqlstate) 482 | end 483 | raise Mysql::ServerError.new(message, @sqlstate, errno) 484 | end 485 | Packet.new(data) 486 | end 487 | 488 | def read_timeout(len, timeout) 489 | return @socket.read(len) if timeout.nil? || timeout == 0 490 | result = '' 491 | e = Time.now + timeout 492 | while result.size < len 493 | now = Time.now 494 | raise Errno::ETIMEDOUT if now > e 495 | r = @socket.read_nonblock(len - result.size, exception: false) 496 | case r 497 | when :wait_readable 498 | IO.select([@socket], nil, nil, e - now) 499 | next 500 | when :wait_writable 501 | IO.select(nil, [@socket], nil, e - now) 502 | next 503 | else 504 | result << r 505 | end 506 | end 507 | return result 508 | end 509 | 510 | # Write one packet data 511 | # @param data [String, IO, nil] packet data. If data is nil, write empty packet. 512 | def write(data) 513 | begin 514 | @socket.sync = false 515 | if data.nil? 516 | write_timeout([0, 0, @seq].pack("CvC"), @opts[:write_timeout]) 517 | @seq = (@seq + 1) % 256 518 | else 519 | data = StringIO.new data if data.is_a? String 520 | while d = data.read(MAX_PACKET_LENGTH) 521 | write_timeout([d.length%256, d.length/256, @seq].pack("CvC")+d, @opts[:write_timeout]) 522 | @seq = (@seq + 1) % 256 523 | end 524 | end 525 | @socket.sync = true 526 | @socket.flush 527 | rescue Errno::EPIPE 528 | @socket.close rescue nil 529 | raise ClientError::ServerGoneError, 'MySQL server has gone away' 530 | rescue Errno::ETIMEDOUT 531 | raise ClientError, "write timeout" 532 | end 533 | end 534 | 535 | def write_timeout(data, timeout) 536 | return @socket.write(data) if timeout.nil? || timeout == 0 537 | len = 0 538 | e = Time.now + timeout 539 | while len < data.size 540 | now = Time.now 541 | raise Errno::ETIMEDOUT if now > e 542 | l = @socket.write_nonblock(data[len..-1], exception: false) 543 | case l 544 | when :wait_readable 545 | IO.select([@socket], nil, nil, e - now) 546 | when :wait_writable 547 | IO.select(nil, [@socket], nil, e - now) 548 | else 549 | len += l 550 | end 551 | end 552 | return len 553 | end 554 | 555 | # Read EOF packet 556 | # @raise [ProtocolError] packet is not EOF 557 | def read_eof_packet 558 | raise ProtocolError, "packet is not EOF" unless read.eof? 559 | end 560 | 561 | # Send simple command 562 | # @param packet :: [String] packet data 563 | # @return [String] received data 564 | def simple_command(packet) 565 | synchronize do 566 | reset 567 | write packet 568 | read.to_s 569 | end 570 | end 571 | 572 | # Initial packet 573 | class InitialPacket 574 | def self.parse(pkt) 575 | protocol_version = pkt.utiny 576 | server_version = pkt.string 577 | thread_id = pkt.ulong 578 | scramble_buff = pkt.read(8) 579 | f0 = pkt.utiny 580 | server_capabilities = pkt.ushort 581 | server_charset = pkt.utiny 582 | server_status = pkt.ushort 583 | server_capabilities2 = pkt.ushort 584 | scramble_length = pkt.utiny 585 | _f1 = pkt.read(10) 586 | rest_scramble_buff = pkt.string 587 | auth_plugin = pkt.string 588 | 589 | server_capabilities |= server_capabilities2 << 16 590 | scramble_buff.concat rest_scramble_buff 591 | 592 | raise ProtocolError, "unsupported version: #{protocol_version}" unless protocol_version == VERSION 593 | raise ProtocolError, "invalid packet: f0=#{f0}" unless f0 == 0 594 | raise ProtocolError, "invalid packet: scramble_length(#{scramble_length}) != length of scramble(#{scramble_buff.size + 1})" unless scramble_length == scramble_buff.size + 1 595 | 596 | self.new protocol_version, server_version, thread_id, server_capabilities, server_charset, server_status, scramble_buff, auth_plugin 597 | end 598 | 599 | attr_reader :protocol_version, :server_version, :thread_id, :server_capabilities, :server_charset, :server_status, :scramble_buff, :auth_plugin 600 | 601 | def initialize(*args) 602 | @protocol_version, @server_version, @thread_id, @server_capabilities, @server_charset, @server_status, @scramble_buff, @auth_plugin = args 603 | end 604 | end 605 | 606 | # Result packet 607 | class ResultPacket 608 | def self.parse(pkt) 609 | field_count = pkt.lcb 610 | if field_count == 0 611 | affected_rows = pkt.lcb 612 | insert_id = pkt.lcb 613 | server_status = pkt.ushort 614 | warning_count = pkt.ushort 615 | message = pkt.lcs 616 | return self.new(field_count, affected_rows, insert_id, server_status, warning_count, message) 617 | elsif field_count.nil? # LOAD DATA LOCAL INFILE 618 | return self.new(nil, nil, nil, nil, nil, pkt.to_s) 619 | else 620 | return self.new(field_count) 621 | end 622 | end 623 | 624 | attr_reader :field_count, :affected_rows, :insert_id, :server_status, :warning_count, :message 625 | 626 | def initialize(*args) 627 | @field_count, @affected_rows, @insert_id, @server_status, @warning_count, @message = args 628 | end 629 | end 630 | 631 | # Field packet 632 | class FieldPacket 633 | def self.parse(pkt) 634 | _first = pkt.lcs 635 | db = pkt.lcs 636 | table = pkt.lcs 637 | org_table = pkt.lcs 638 | name = pkt.lcs 639 | org_name = pkt.lcs 640 | _f0 = pkt.utiny 641 | charsetnr = pkt.ushort 642 | length = pkt.ulong 643 | type = pkt.utiny 644 | flags = pkt.ushort 645 | decimals = pkt.utiny 646 | f1 = pkt.ushort 647 | 648 | raise ProtocolError, "invalid packet: f1=#{f1}" unless f1 == 0 649 | default = pkt.lcs 650 | return self.new(db, table, org_table, name, org_name, charsetnr, length, type, flags, decimals, default) 651 | end 652 | 653 | attr_reader :db, :table, :org_table, :name, :org_name, :charsetnr, :length, :type, :flags, :decimals, :default 654 | 655 | def initialize(*args) 656 | @db, @table, @org_table, @name, @org_name, @charsetnr, @length, @type, @flags, @decimals, @default = args 657 | end 658 | end 659 | 660 | # Prepare result packet 661 | class PrepareResultPacket 662 | def self.parse(pkt) 663 | raise ProtocolError, "invalid packet" unless pkt.utiny == 0 664 | statement_id = pkt.ulong 665 | field_count = pkt.ushort 666 | param_count = pkt.ushort 667 | f = pkt.utiny 668 | warning_count = pkt.ushort 669 | raise ProtocolError, "invalid packet" unless f == 0x00 670 | self.new statement_id, field_count, param_count, warning_count 671 | end 672 | 673 | attr_reader :statement_id, :field_count, :param_count, :warning_count 674 | 675 | def initialize(*args) 676 | @statement_id, @field_count, @param_count, @warning_count = args 677 | end 678 | end 679 | 680 | # Authentication packet 681 | class AuthenticationPacket 682 | def self.serialize(client_flags, max_packet_size, charset_number, username, scrambled_password, databasename, auth_plugin) 683 | data = [ 684 | client_flags, 685 | max_packet_size, 686 | charset_number, 687 | "", # always 0x00 * 23 688 | username, 689 | Packet.lcs(scrambled_password), 690 | ] 691 | pack = "VVCa23Z*A*" 692 | if databasename 693 | data.push databasename 694 | pack.concat "Z*" 695 | end 696 | data.push auth_plugin 697 | pack.concat "Z*" 698 | data.pack(pack) 699 | end 700 | end 701 | 702 | # TLS Authentication packet 703 | class TlsAuthenticationPacket 704 | def self.serialize(client_flags, max_packet_size, charset_number) 705 | [ 706 | client_flags, 707 | max_packet_size, 708 | charset_number, 709 | "", # always 0x00 * 23 710 | ].pack("VVCa23") 711 | end 712 | end 713 | 714 | # Execute packet 715 | class ExecutePacket 716 | def self.serialize(statement_id, cursor_type, values) 717 | nbm = null_bitmap values 718 | netvalues = "" 719 | types = values.map do |v| 720 | t, n = Protocol.value2net v 721 | netvalues.concat n if v 722 | t 723 | end 724 | [Mysql::COM_STMT_EXECUTE, statement_id, cursor_type, 1, nbm, 1, types.pack("v*"), netvalues].pack("CVCVa*Ca*a*") 725 | end 726 | 727 | # make null bitmap 728 | # 729 | # If values is [1, nil, 2, 3, nil] then returns "\x12"(0b10010). 730 | def self.null_bitmap(values) 731 | bitmap = values.enum_for(:each_slice,8).map do |vals| 732 | vals.reverse.inject(0){|b, v|(b << 1 | (v ? 0 : 1))} 733 | end 734 | return bitmap.pack("C*") 735 | end 736 | 737 | end 738 | 739 | class AuthenticationResultPacket 740 | def self.parse(pkt) 741 | result = pkt.utiny 742 | auth_plugin = pkt.string 743 | scramble = pkt.string 744 | self.new(result, auth_plugin, scramble) 745 | end 746 | 747 | attr_reader :result, :auth_plugin, :scramble 748 | 749 | def initialize(*args) 750 | @result, @auth_plugin, @scramble = args 751 | end 752 | end 753 | end 754 | 755 | class RawRecord 756 | def initialize(packet, fields, encoding) 757 | @packet, @fields, @encoding = packet, fields, encoding 758 | end 759 | 760 | def to_a 761 | @fields.map do |f| 762 | if s = @packet.lcs 763 | unless f.type == Field::TYPE_BIT or f.charsetnr == Charset::BINARY_CHARSET_NUMBER 764 | s = Charset.convert_encoding(s, @encoding) 765 | end 766 | end 767 | s 768 | end 769 | end 770 | end 771 | 772 | class StmtRawRecord 773 | # @param pkt [Packet] 774 | # @param fields [Array of Fields] 775 | # @param encoding [Encoding] 776 | def initialize(packet, fields, encoding) 777 | @packet, @fields, @encoding = packet, fields, encoding 778 | end 779 | 780 | # Parse statement result packet 781 | # @return [Array] one record 782 | def parse_record_packet 783 | @packet.utiny # skip first byte 784 | null_bit_map = @packet.read((@fields.length+7+2)/8).unpack("b*").first 785 | rec = @fields.each_with_index.map do |f, i| 786 | if null_bit_map[i+2] == ?1 787 | nil 788 | else 789 | unsigned = f.flags & Field::UNSIGNED_FLAG != 0 790 | v = Protocol.net2value(@packet, f.type, unsigned) 791 | if v.nil? or v.is_a? Numeric or v.is_a? Time 792 | v 793 | elsif f.type == Field::TYPE_BIT or f.charsetnr == Charset::BINARY_CHARSET_NUMBER 794 | Charset.to_binary(v) 795 | else 796 | Charset.convert_encoding(v, @encoding) 797 | end 798 | end 799 | end 800 | rec 801 | end 802 | 803 | alias to_a parse_record_packet 804 | 805 | end 806 | end 807 | -------------------------------------------------------------------------------- /lib/mysql.rb: -------------------------------------------------------------------------------- 1 | # coding: ascii-8bit 2 | # Copyright (C) 2008 TOMITA Masahiro 3 | # mailto:tommy@tmtm.org 4 | 5 | require 'uri' 6 | 7 | # MySQL connection class. 8 | # @example 9 | # my = Mysql.connect('hostname', 'user', 'password', 'dbname') 10 | # res = my.query 'select col1,col2 from tbl where id=123' 11 | # res.each do |c1, c2| 12 | # p c1, c2 13 | # end 14 | class Mysql 15 | 16 | require_relative "mysql/constants" 17 | require_relative "mysql/error" 18 | require_relative "mysql/charset" 19 | require_relative "mysql/protocol" 20 | require_relative "mysql/packet.rb" 21 | 22 | VERSION = '3.0.1' # Version number of this library 23 | MYSQL_UNIX_PORT = "/tmp/mysql.sock" # UNIX domain socket filename 24 | MYSQL_TCP_PORT = 3306 # TCP socket port number 25 | 26 | # @!attribute [rw] host 27 | # @return [String, nil] 28 | # @!attribute [rw] username 29 | # @return [String, nil] 30 | # @!attribute [rw] password 31 | # @return [String, nil] 32 | # @!attribute [rw] database 33 | # @return [String, nil] 34 | # @!attribute [rw] port 35 | # @return [Integer, String, nil] 36 | # @!attribute [rw] socket 37 | # @return [String, nil] socket filename 38 | # @!attribute [rw] flags 39 | # @return [Integer, nil] 40 | # @!attribute [rw] connect_timeout 41 | # @return [Numeric, nil] 42 | # @!attribute [rw] read_timeout 43 | # @return [Numeric, nil] 44 | # @!attribute [rw] write_timeout 45 | # @return [Numeric, nil] 46 | # @!attribute [rw] init_command 47 | # @return [String, nil] 48 | # @!attribute [rw] local_infile 49 | # @return [Boolean] 50 | # @!attribute [rw] load_data_local_dir 51 | # @return [String, nil] 52 | # @!attribute [rw] ssl_mode 53 | # @return [String, Integer] 1 or "disabled" / 2 or "preferred" / 3 or "required" 54 | # @!attribute [rw] get_server_public_key 55 | # @return [Boolean] 56 | DEFAULT_OPTS = { 57 | host: nil, 58 | username: nil, 59 | password: nil, 60 | database: nil, 61 | port: nil, 62 | socket: nil, 63 | flags: 0, 64 | charset: nil, 65 | connect_timeout: nil, 66 | read_timeout: nil, 67 | write_timeout: nil, 68 | init_command: nil, 69 | local_infile: nil, 70 | load_data_local_dir: nil, 71 | ssl_mode: SSL_MODE_PREFERRED, 72 | get_server_public_key: false, 73 | }.freeze 74 | 75 | # @private 76 | attr_reader :protocol 77 | 78 | class << self 79 | # Make Mysql object and connect to mysqld. 80 | # parameter is same as arguments for {#initialize}. 81 | # @return [Mysql] 82 | def connect(*args, **opts) 83 | self.new(*args, **opts).connect 84 | end 85 | 86 | # Escape special character in string. 87 | # @param [String] str 88 | # @return [String] 89 | def escape_string(str) 90 | str.gsub(/[\0\n\r\\\'\"\x1a]/) do |s| 91 | case s 92 | when "\0" then "\\0" 93 | when "\n" then "\\n" 94 | when "\r" then "\\r" 95 | when "\x1a" then "\\Z" 96 | else "\\#{s}" 97 | end 98 | end 99 | end 100 | alias quote escape_string 101 | end 102 | 103 | # @overload initialize(uri, **opts) 104 | # @param uri [String, URI] "mysql://username:password@host:port/database?param=value&..." / "mysql://username:password@%2Ftmp%2Fmysql.sock/database" / "mysql://username:password@/database?socket=/tmp/mysql.sock" 105 | # @param opts [Hash] options 106 | # @overload initialize(host, username, password, database, port, socket, flags, **opts) 107 | # @param host [String] hostname mysqld running 108 | # @param username [String] username to connect to mysqld 109 | # @param password [String] password to connect to mysqld 110 | # @param database [String] initial database name 111 | # @param port [String] port number (used if host is not 'localhost' or nil) 112 | # @param socket [String] socket filename (used if host is 'localhost' or nil) 113 | # @param flags [Integer] connection flag. Mysql::CLIENT_* ORed 114 | # @param opts [Hash] options 115 | # @overload initialize(host: nil, username: nil, password: nil, database: nil, port: nil, socket: nil, flags: nil, **opts) 116 | # @param host [String] hostname mysqld running 117 | # @param username [String] username to connect to mysqld 118 | # @param password [String] password to connect to mysqld 119 | # @param database [String] initial database name 120 | # @param port [String] port number (used if host is not 'localhost' or nil) 121 | # @param socket [String] socket filename (used if host is 'localhost' or nil) 122 | # @param flags [Integer] connection flag. Mysql::CLIENT_* ORed 123 | # @param opts [Hash] options 124 | # @option opts :host [String] hostname mysqld running 125 | # @option opts :username [String] username to connect to mysqld 126 | # @option opts :password [String] password to connect to mysqld 127 | # @option opts :database [String] initial database name 128 | # @option opts :port [String] port number (used if host is not 'localhost' or nil) 129 | # @option opts :socket [String] socket filename (used if host is 'localhost' or nil) 130 | # @option opts :flags [Integer] connection flag. Mysql::CLIENT_* ORed 131 | # @option opts :charset [Mysql::Charset, String] character set 132 | # @option opts :connect_timeout [Numeric, nil] 133 | # @option opts :read_timeout [Numeric, nil] 134 | # @option opts :write_timeout [Numeric, nil] 135 | # @option opts :local_infile [Boolean] 136 | # @option opts :load_data_local_dir [String] 137 | # @option opts :ssl_mode [Integer] 138 | # @option opts :get_server_public_key [Boolean] 139 | def initialize(*args, **opts) 140 | @fields = nil 141 | @protocol = nil 142 | @sqlstate = "00000" 143 | @host_info = nil 144 | @last_error = nil 145 | @result_exist = false 146 | @opts = DEFAULT_OPTS.dup 147 | parse_args(args, opts) 148 | end 149 | 150 | # Connect to mysqld. 151 | # parameter is same as arguments for {#initialize}. 152 | # @return [Mysql] self 153 | def connect(*args, **opts) 154 | parse_args(args, opts) 155 | if @opts[:flags] & CLIENT_COMPRESS != 0 156 | warn 'unsupported flag: CLIENT_COMPRESS' if $VERBOSE 157 | @opts[:flags] &= ~CLIENT_COMPRESS 158 | end 159 | @protocol = Protocol.new(@opts) 160 | @protocol.authenticate 161 | @host_info = (@opts[:host].nil? || @opts[:host] == "localhost") ? 'Localhost via UNIX socket' : "#{@opts[:host]} via TCP/IP" 162 | query @opts[:init_command] if @opts[:init_command] 163 | return self 164 | end 165 | 166 | def parse_args(args, opts) 167 | case args[0] 168 | when URI 169 | uri = args[0] 170 | when /\Amysql:\/\// 171 | uri = URI.parse(args[0]) 172 | when String 173 | @opts[:host], user, passwd, dbname, port, socket, flags = *args 174 | @opts[:username] = user if user 175 | @opts[:password] = passwd if passwd 176 | @opts[:database] = dbname if dbname 177 | @opts[:port] = port if port 178 | @opts[:socket] = socket if socket 179 | @opts[:flags] = flags if flags 180 | when Hash 181 | # skip 182 | when nil 183 | # skip 184 | end 185 | if uri 186 | host = uri.hostname.to_s 187 | host = URI.decode_www_form_component(host) 188 | if host.start_with?('/') 189 | @opts[:socket] = host 190 | host = '' 191 | end 192 | @opts[:host] = host 193 | @opts[:username] = URI.decode_www_form_component(uri.user.to_s) 194 | @opts[:password] = URI.decode_www_form_component(uri.password.to_s) 195 | @opts[:database] = uri.path.sub(/\A\/+/, '') 196 | @opts[:port] = uri.port 197 | opts = URI.decode_www_form(uri.query).to_h.transform_keys(&:intern).merge(opts) if uri.query 198 | opts[:flags] = opts[:flags].to_i if opts[:flags] 199 | end 200 | if args.last.kind_of? Hash 201 | opts = opts.merge(args.last) 202 | end 203 | @opts.update(opts) 204 | end 205 | 206 | DEFAULT_OPTS.each_key do |var| 207 | next if var == :charset 208 | define_method(var){@opts[var]} 209 | define_method("#{var}="){|val| @opts[var] = val} 210 | end 211 | 212 | # Disconnect from mysql. 213 | # @return [Mysql] self 214 | def close 215 | if @protocol 216 | @protocol.quit_command 217 | @protocol = nil 218 | end 219 | return self 220 | end 221 | 222 | # Disconnect from mysql without QUIT packet. 223 | # @return [Mysql] self 224 | def close! 225 | if @protocol 226 | @protocol.close 227 | @protocol = nil 228 | end 229 | return self 230 | end 231 | 232 | # Escape special character in MySQL. 233 | # 234 | # @param [String] str 235 | # return [String] 236 | def escape_string(str) 237 | self.class.escape_string str 238 | end 239 | alias quote escape_string 240 | 241 | # @return [Mysql::Charset] character set of MySQL connection 242 | def charset 243 | @opts[:charset] 244 | end 245 | 246 | # Set charset of MySQL connection. 247 | # @param [String, Mysql::Charset] cs 248 | def charset=(cs) 249 | charset = cs.is_a?(Charset) ? cs : Charset.by_name(cs) 250 | if @protocol 251 | @protocol.charset = charset 252 | query "SET NAMES #{charset.name}" 253 | end 254 | @opts[:charset] = charset 255 | cs 256 | end 257 | 258 | # @return [String] charset name 259 | def character_set_name 260 | @protocol.charset.name 261 | end 262 | 263 | # @return [Integer] last error number 264 | def errno 265 | @last_error ? @last_error.errno : 0 266 | end 267 | 268 | # @return [String] last error message 269 | def error 270 | @last_error && @last_error.error 271 | end 272 | 273 | # @return [String] sqlstate for last error 274 | def sqlstate 275 | @last_error ? @last_error.sqlstate : "00000" 276 | end 277 | 278 | # @return [Integer] number of columns for last query 279 | def field_count 280 | @fields.size 281 | end 282 | 283 | # @return [String] connection type 284 | def host_info 285 | @host_info 286 | end 287 | 288 | # @return [String] server version 289 | def server_info 290 | check_connection 291 | @protocol.server_info 292 | end 293 | 294 | # @return [Integer] server version 295 | def server_version 296 | check_connection 297 | @protocol.server_version 298 | end 299 | 300 | # @return [String] information for last query 301 | def info 302 | @protocol && @protocol.message 303 | end 304 | 305 | # @return [Integer] number of affected records by insert/update/delete. 306 | def affected_rows 307 | @protocol ? @protocol.affected_rows : 0 308 | end 309 | 310 | # @return [Integer] latest auto_increment value 311 | def insert_id 312 | @protocol ? @protocol.insert_id : 0 313 | end 314 | 315 | # @return [Integer] number of warnings for previous query 316 | def warning_count 317 | @protocol ? @protocol.warning_count : 0 318 | end 319 | 320 | # Kill query. 321 | # @param [Integer] pid thread id 322 | # @return [Mysql] self 323 | def kill(pid) 324 | check_connection 325 | @protocol.kill_command pid 326 | self 327 | end 328 | 329 | # Execute query string. 330 | # @overload query(str) 331 | # @param [String] str Query. 332 | # @return [Mysql::Result] 333 | # @return [nil] if the query does not return result set. 334 | # @overload query(str, &block) 335 | # @param [String] str Query. 336 | # @yield [Mysql::Result] evaluated per query. 337 | # @return [self] 338 | # @example 339 | # my.query("select 1,NULL,'abc'").fetch # => [1, nil, "abc"] 340 | # my.query("select 1,NULL,'abc'"){|res| res.fetch} 341 | def query(str, &block) 342 | check_connection 343 | @fields = nil 344 | begin 345 | nfields = @protocol.query_command str 346 | if nfields 347 | @fields = @protocol.retr_fields nfields 348 | @result_exist = true 349 | end 350 | if block 351 | while true 352 | block.call store_result if @fields 353 | break unless next_result 354 | end 355 | return self 356 | end 357 | return @fields ? store_result : nil 358 | rescue ServerError => e 359 | @last_error = e 360 | @sqlstate = e.sqlstate 361 | raise 362 | end 363 | end 364 | 365 | # Get all data for last query. 366 | # @return [Mysql::Result] 367 | def store_result 368 | check_connection 369 | raise ClientError, 'invalid usage' unless @result_exist 370 | res = Result.new @fields, @protocol 371 | @result_exist = false 372 | res 373 | end 374 | 375 | # @return [Integer] Thread ID 376 | def thread_id 377 | check_connection 378 | @protocol.thread_id 379 | end 380 | 381 | # Set server option. 382 | # @param [Integer] opt {Mysql::OPTION_MULTI_STATEMENTS_ON} or {Mysql::OPTION_MULTI_STATEMENTS_OFF} 383 | # @return [Mysql] self 384 | def set_server_option(opt) 385 | check_connection 386 | @protocol.set_option_command opt 387 | self 388 | end 389 | 390 | # @return [Boolean] true if multiple queries are specified and unexecuted queries exists. 391 | def more_results 392 | @protocol.server_status & SERVER_MORE_RESULTS_EXISTS != 0 393 | end 394 | alias more_results? more_results 395 | 396 | # execute next query if multiple queries are specified. 397 | # @return [Boolean] true if next query exists. 398 | def next_result 399 | return false unless more_results 400 | check_connection 401 | @fields = nil 402 | nfields = @protocol.get_result 403 | if nfields 404 | @fields = @protocol.retr_fields nfields 405 | @result_exist = true 406 | end 407 | return true 408 | end 409 | 410 | # Parse prepared-statement. 411 | # @param [String] str query string 412 | # @return [Mysql::Stmt] Prepared-statement object 413 | def prepare(str) 414 | st = Stmt.new @protocol 415 | st.prepare str 416 | st 417 | end 418 | 419 | # @private 420 | # Make empty prepared-statement object. 421 | # @return [Mysql::Stmt] If block is not specified. 422 | def stmt 423 | Stmt.new @protocol 424 | end 425 | 426 | # Check whether the connection is available. 427 | # @return [Mysql] self 428 | def ping 429 | check_connection 430 | @protocol.ping_command 431 | self 432 | end 433 | 434 | # Flush tables or caches. 435 | # @param [Integer] op operation. Use Mysql::REFRESH_* value. 436 | # @return [Mysql] self 437 | def refresh(op) 438 | check_connection 439 | @protocol.refresh_command op 440 | self 441 | end 442 | 443 | # Reload grant tables. 444 | # @return [Mysql] self 445 | def reload 446 | refresh Mysql::REFRESH_GRANT 447 | end 448 | 449 | # Select default database 450 | # @return [Mysql] self 451 | def select_db(db) 452 | query "use #{db}" 453 | self 454 | end 455 | 456 | # shutdown server. 457 | # @return [Mysql] self 458 | def shutdown(level=0) 459 | check_connection 460 | @protocol.shutdown_command level 461 | self 462 | end 463 | 464 | # @return [String] statistics message 465 | def stat 466 | @protocol ? @protocol.statistics_command : 'MySQL server has gone away' 467 | end 468 | 469 | # Commit transaction 470 | # @return [Mysql] self 471 | def commit 472 | query 'commit' 473 | self 474 | end 475 | 476 | # Rollback transaction 477 | # @return [Mysql] self 478 | def rollback 479 | query 'rollback' 480 | self 481 | end 482 | 483 | # Set autocommit mode 484 | # @param [Boolean] flag 485 | # @return [Mysql] self 486 | def autocommit(flag) 487 | query "set autocommit=#{flag ? 1 : 0}" 488 | self 489 | end 490 | 491 | private 492 | 493 | def check_connection 494 | raise ClientError::ServerGoneError, 'MySQL server has gone away' unless @protocol 495 | end 496 | 497 | # @!visibility public 498 | # Field class 499 | class Field 500 | # @return [String] database name 501 | attr_reader :db 502 | # @return [String] table name 503 | attr_reader :table 504 | # @return [String] original table name 505 | attr_reader :org_table 506 | # @return [String] field name 507 | attr_reader :name 508 | # @return [String] original field name 509 | attr_reader :org_name 510 | # @return [Integer] charset id number 511 | attr_reader :charsetnr 512 | # @return [Integer] field length 513 | attr_reader :length 514 | # @return [Integer] field type 515 | attr_reader :type 516 | # @return [Integer] flag 517 | attr_reader :flags 518 | # @return [Integer] number of decimals 519 | attr_reader :decimals 520 | # @return [String] defualt value 521 | attr_reader :default 522 | alias :def :default 523 | 524 | # @private 525 | attr_accessor :result 526 | 527 | # @attr [Protocol::FieldPacket] packet 528 | def initialize(packet) 529 | @db, @table, @org_table, @name, @org_name, @charsetnr, @length, @type, @flags, @decimals, @default = 530 | packet.db, packet.table, packet.org_table, packet.name, packet.org_name, packet.charsetnr, packet.length, packet.type, packet.flags, packet.decimals, packet.default 531 | @flags |= NUM_FLAG if is_num_type? 532 | @max_length = nil 533 | end 534 | 535 | # @return [Hash] field information 536 | def to_hash 537 | { 538 | "name" => @name, 539 | "table" => @table, 540 | "def" => @default, 541 | "type" => @type, 542 | "length" => @length, 543 | "max_length" => max_length, 544 | "flags" => @flags, 545 | "decimals" => @decimals 546 | } 547 | end 548 | 549 | # @private 550 | def inspect 551 | "#" 552 | end 553 | 554 | # @return [Boolean] true if numeric field. 555 | def is_num? 556 | @flags & NUM_FLAG != 0 557 | end 558 | 559 | # @return [Boolean] true if not null field. 560 | def is_not_null? 561 | @flags & NOT_NULL_FLAG != 0 562 | end 563 | 564 | # @return [Boolean] true if primary key field. 565 | def is_pri_key? 566 | @flags & PRI_KEY_FLAG != 0 567 | end 568 | 569 | # @return [Integer] maximum width of the field for the result set 570 | def max_length 571 | return @max_length if @max_length 572 | @max_length = 0 573 | @result.calculate_field_max_length if @result 574 | @max_length 575 | end 576 | 577 | attr_writer :max_length 578 | 579 | private 580 | 581 | def is_num_type? 582 | [TYPE_DECIMAL, TYPE_TINY, TYPE_SHORT, TYPE_LONG, TYPE_FLOAT, TYPE_DOUBLE, TYPE_LONGLONG, TYPE_INT24].include?(@type) || (@type == TYPE_TIMESTAMP && (@length == 14 || @length == 8)) 583 | end 584 | 585 | end 586 | 587 | # @!visibility public 588 | # Result set 589 | class ResultBase 590 | include Enumerable 591 | 592 | # @return [Array] field list 593 | attr_reader :fields 594 | 595 | # @param [Array of Mysql::Field] fields 596 | def initialize(fields) 597 | @fields = fields 598 | @field_index = 0 # index of field 599 | @records = [] # all records 600 | @index = 0 # index of record 601 | @fieldname_with_table = nil 602 | @fetched_record = nil 603 | end 604 | 605 | # ignore 606 | # @return [void] 607 | def free 608 | end 609 | 610 | # @return [Integer] number of record 611 | def size 612 | @records.size 613 | end 614 | alias num_rows size 615 | 616 | # @return [Array] current record data 617 | def fetch 618 | @fetched_record = nil 619 | return nil if @index >= @records.size 620 | @records[@index] = @records[@index].to_a unless @records[@index].is_a? Array 621 | @fetched_record = @records[@index] 622 | @index += 1 623 | return @fetched_record 624 | end 625 | alias fetch_row fetch 626 | 627 | # Return data of current record as Hash. 628 | # The hash key is field name. 629 | # @param [Boolean] with_table if true, hash key is "table_name.field_name". 630 | # @return [Hash] current record data 631 | def fetch_hash(with_table=nil) 632 | row = fetch 633 | return nil unless row 634 | if with_table and @fieldname_with_table.nil? 635 | @fieldname_with_table = @fields.map{|f| [f.table, f.name].join(".")} 636 | end 637 | ret = {} 638 | @fields.each_index do |i| 639 | fname = with_table ? @fieldname_with_table[i] : @fields[i].name 640 | ret[fname] = row[i] 641 | end 642 | ret 643 | end 644 | 645 | # Iterate block with record. 646 | # @yield [Array] record data 647 | # @return [self] self. If block is not specified, this returns Enumerator. 648 | def each(&block) 649 | return enum_for(:each) unless block 650 | while rec = fetch 651 | block.call rec 652 | end 653 | self 654 | end 655 | 656 | # Iterate block with record as Hash. 657 | # @param [Boolean] with_table if true, hash key is "table_name.field_name". 658 | # @yield [Hash] record data 659 | # @return [self] self. If block is not specified, this returns Enumerator. 660 | def each_hash(with_table=nil, &block) 661 | return enum_for(:each_hash, with_table) unless block 662 | while rec = fetch_hash(with_table) 663 | block.call rec 664 | end 665 | self 666 | end 667 | 668 | # Set record position 669 | # @param [Integer] n record index 670 | # @return [self] self 671 | def data_seek(n) 672 | @index = n 673 | self 674 | end 675 | 676 | # @return [Integer] current record position 677 | def row_tell 678 | @index 679 | end 680 | 681 | # Set current position of record 682 | # @param [Integer] n record index 683 | # @return [Integer] previous position 684 | def row_seek(n) 685 | ret = @index 686 | @index = n 687 | ret 688 | end 689 | end 690 | 691 | # @!visibility public 692 | # Result set for simple query 693 | class Result < ResultBase 694 | # @private 695 | # @param [Array] fields 696 | # @param [Mysql::Protocol] protocol 697 | def initialize(fields, protocol=nil) 698 | super fields 699 | return unless protocol 700 | @records = protocol.retr_all_records fields 701 | fields.each{|f| f.result = self} # for calculating max_field 702 | end 703 | 704 | # @private 705 | # calculate max_length of all fields 706 | def calculate_field_max_length 707 | max_length = Array.new(@fields.size, 0) 708 | @records.each_with_index do |rec, i| 709 | rec = @records[i] = rec.to_a if rec.is_a? RawRecord 710 | max_length.each_index do |j| 711 | max_length[j] = rec[j].length if rec[j] && rec[j].length > max_length[j] 712 | end 713 | end 714 | max_length.each_with_index do |len, i| 715 | @fields[i].max_length = len 716 | end 717 | end 718 | 719 | # @return [Mysql::Field] current field 720 | def fetch_field 721 | return nil if @field_index >= @fields.length 722 | ret = @fields[@field_index] 723 | @field_index += 1 724 | ret 725 | end 726 | 727 | # @return [Integer] current field position 728 | def field_tell 729 | @field_index 730 | end 731 | 732 | # Set field position 733 | # @param [Integer] n field index 734 | # @return [Integer] previous position 735 | def field_seek(n) 736 | ret = @field_index 737 | @field_index = n 738 | ret 739 | end 740 | 741 | # Return specified field 742 | # @param [Integer] n field index 743 | # @return [Mysql::Field] field 744 | def fetch_field_direct(n) 745 | raise ClientError, "invalid argument: #{n}" if n < 0 or n >= @fields.length 746 | @fields[n] 747 | end 748 | 749 | # @return [Array] all fields 750 | def fetch_fields 751 | @fields 752 | end 753 | 754 | # @return [Array] length of each fields 755 | def fetch_lengths 756 | return nil unless @fetched_record 757 | @fetched_record.map{|c|c.nil? ? 0 : c.length} 758 | end 759 | 760 | # @return [Integer] number of fields 761 | def num_fields 762 | @fields.size 763 | end 764 | end 765 | 766 | # @!visibility private 767 | # Result set for prepared statement 768 | class StatementResult < ResultBase 769 | # @private 770 | # @param [Array] fields 771 | # @param [Mysql::Protocol] protocol 772 | def initialize(fields, protocol) 773 | super fields 774 | @records = protocol.stmt_retr_all_records @fields, protocol.charset 775 | end 776 | end 777 | 778 | # @!visibility public 779 | # Prepared statement 780 | # @!attribute [r] affected_rows 781 | # @return [Integer] 782 | # @!attribute [r] insert_id 783 | # @return [Integer] 784 | # @!attribute [r] server_status 785 | # @return [Integer] 786 | # @!attribute [r] warning_count 787 | # @return [Integer] 788 | # @!attribute [r] param_count 789 | # @return [Integer] 790 | # @!attribute [r] fields 791 | # @return [Array] 792 | # @!attribute [r] sqlstate 793 | # @return [String] 794 | class Stmt 795 | include Enumerable 796 | 797 | attr_reader :affected_rows, :insert_id, :server_status, :warning_count 798 | attr_reader :param_count, :fields, :sqlstate 799 | 800 | # @private 801 | def self.finalizer(protocol, statement_id) 802 | proc do 803 | protocol.gc_stmt statement_id 804 | end 805 | end 806 | 807 | # @private 808 | # @param [Mysql::Protocol] protocol 809 | def initialize(protocol) 810 | @protocol = protocol 811 | @statement_id = nil 812 | @affected_rows = @insert_id = @server_status = @warning_count = 0 813 | @sqlstate = "00000" 814 | @param_count = nil 815 | end 816 | 817 | # @private 818 | # parse prepared-statement and return {Mysql::Stmt} object 819 | # @param [String] str query string 820 | # @return self 821 | def prepare(str) 822 | close 823 | begin 824 | @sqlstate = "00000" 825 | @statement_id, @param_count, @fields = @protocol.stmt_prepare_command(str) 826 | rescue ServerError => e 827 | @last_error = e 828 | @sqlstate = e.sqlstate 829 | raise 830 | end 831 | ObjectSpace.define_finalizer(self, self.class.finalizer(@protocol, @statement_id)) 832 | self 833 | end 834 | 835 | # Execute prepared statement. 836 | # @param [Object] values values passed to query 837 | # @return [Mysql::Stmt] self 838 | def execute(*values) 839 | raise ClientError, "not prepared" unless @param_count 840 | raise ClientError, "parameter count mismatch" if values.length != @param_count 841 | values = values.map{|v| @protocol.charset.convert v} 842 | begin 843 | @sqlstate = "00000" 844 | nfields = @protocol.stmt_execute_command @statement_id, values 845 | if nfields 846 | @fields = @protocol.retr_fields nfields 847 | @result = StatementResult.new @fields, @protocol 848 | else 849 | @affected_rows, @insert_id, @server_status, @warning_count, @info = 850 | @protocol.affected_rows, @protocol.insert_id, @protocol.server_status, @protocol.warning_count, @protocol.message 851 | end 852 | return self 853 | rescue ServerError => e 854 | @last_error = e 855 | @sqlstate = e.sqlstate 856 | raise 857 | end 858 | end 859 | 860 | # Close prepared statement 861 | # @return [void] 862 | def close 863 | ObjectSpace.undefine_finalizer(self) 864 | @protocol.stmt_close_command @statement_id if @statement_id 865 | @statement_id = nil 866 | end 867 | 868 | # @return [Array] current record data 869 | def fetch 870 | @result.fetch 871 | end 872 | 873 | # Return data of current record as Hash. 874 | # The hash key is field name. 875 | # @param [Boolean] with_table if true, hash key is "table_name.field_name". 876 | # @return [Hash] record data 877 | def fetch_hash(with_table=nil) 878 | @result.fetch_hash with_table 879 | end 880 | 881 | # Iterate block with record. 882 | # @yield [Array] record data 883 | # @return [Mysql::Stmt] self 884 | # @return [Enumerator] If block is not specified 885 | def each(&block) 886 | return enum_for(:each) unless block 887 | while rec = fetch 888 | block.call rec 889 | end 890 | self 891 | end 892 | 893 | # Iterate block with record as Hash. 894 | # @param [Boolean] with_table if true, hash key is "table_name.field_name". 895 | # @yield [Hash] record data 896 | # @return [Mysql::Stmt] self 897 | # @return [Enumerator] If block is not specified 898 | def each_hash(with_table=nil, &block) 899 | return enum_for(:each_hash, with_table) unless block 900 | while rec = fetch_hash(with_table) 901 | block.call rec 902 | end 903 | self 904 | end 905 | 906 | # @return [Integer] number of record 907 | def size 908 | @result.size 909 | end 910 | alias num_rows size 911 | 912 | # Set record position 913 | # @param [Integer] n record index 914 | # @return [void] 915 | def data_seek(n) 916 | @result.data_seek(n) 917 | end 918 | 919 | # @return [Integer] current record position 920 | def row_tell 921 | @result.row_tell 922 | end 923 | 924 | # Set current position of record 925 | # @param [Integer] n record index 926 | # @return [Integer] previous position 927 | def row_seek(n) 928 | @result.row_seek(n) 929 | end 930 | 931 | # @return [Integer] number of columns for last query 932 | def field_count 933 | @fields.length 934 | end 935 | 936 | # ignore 937 | # @return [void] 938 | def free_result 939 | end 940 | 941 | # Returns Mysql::Result object that is empty. 942 | # Use fetch_fields to get list of fields. 943 | # @return [Mysql::Result] 944 | def result_metadata 945 | return nil if @fields.empty? 946 | Result.new @fields 947 | end 948 | end 949 | end 950 | -------------------------------------------------------------------------------- /setup.rb: -------------------------------------------------------------------------------- 1 | # 2 | # setup.rb 3 | # 4 | # Copyright (c) 2000-2005 Minero Aoki 5 | # 6 | # This program is free software. 7 | # You can distribute/modify this program under the terms of 8 | # the GNU LGPL, Lesser General Public License version 2.1. 9 | # 10 | 11 | unless Enumerable.method_defined?(:map) # Ruby 1.4.6 12 | module Enumerable 13 | alias map collect 14 | end 15 | end 16 | 17 | unless File.respond_to?(:read) # Ruby 1.6 18 | def File.read(fname) 19 | open(fname) {|f| 20 | return f.read 21 | } 22 | end 23 | end 24 | 25 | unless Errno.const_defined?(:ENOTEMPTY) # Windows? 26 | module Errno 27 | class ENOTEMPTY 28 | # We do not raise this exception, implementation is not needed. 29 | end 30 | end 31 | end 32 | 33 | def File.binread(fname) 34 | open(fname, 'rb') {|f| 35 | return f.read 36 | } 37 | end 38 | 39 | # for corrupted Windows' stat(2) 40 | def File.dir?(path) 41 | File.directory?((path[-1,1] == '/') ? path : path + '/') 42 | end 43 | 44 | 45 | class ConfigTable 46 | 47 | include Enumerable 48 | 49 | def initialize(rbconfig) 50 | @rbconfig = rbconfig 51 | @items = [] 52 | @table = {} 53 | # options 54 | @install_prefix = nil 55 | @config_opt = nil 56 | @verbose = true 57 | @no_harm = false 58 | end 59 | 60 | attr_accessor :install_prefix 61 | attr_accessor :config_opt 62 | 63 | attr_writer :verbose 64 | 65 | def verbose? 66 | @verbose 67 | end 68 | 69 | attr_writer :no_harm 70 | 71 | def no_harm? 72 | @no_harm 73 | end 74 | 75 | def [](key) 76 | lookup(key).resolve(self) 77 | end 78 | 79 | def []=(key, val) 80 | lookup(key).set val 81 | end 82 | 83 | def names 84 | @items.map {|i| i.name } 85 | end 86 | 87 | def each(&block) 88 | @items.each(&block) 89 | end 90 | 91 | def key?(name) 92 | @table.key?(name) 93 | end 94 | 95 | def lookup(name) 96 | @table[name] or setup_rb_error "no such config item: #{name}" 97 | end 98 | 99 | def add(item) 100 | @items.push item 101 | @table[item.name] = item 102 | end 103 | 104 | def remove(name) 105 | item = lookup(name) 106 | @items.delete_if {|i| i.name == name } 107 | @table.delete_if {|name, i| i.name == name } 108 | item 109 | end 110 | 111 | def load_script(path, inst = nil) 112 | if File.file?(path) 113 | MetaConfigEnvironment.new(self, inst).instance_eval File.read(path), path 114 | end 115 | end 116 | 117 | def savefile 118 | '.config' 119 | end 120 | 121 | def load_savefile 122 | begin 123 | File.foreach(savefile()) do |line| 124 | k, v = *line.split(/=/, 2) 125 | self[k] = v.strip 126 | end 127 | rescue Errno::ENOENT 128 | setup_rb_error $!.message + "\n#{File.basename($0)} config first" 129 | end 130 | end 131 | 132 | def save 133 | @items.each {|i| i.value } 134 | File.open(savefile(), 'w') {|f| 135 | @items.each do |i| 136 | f.printf "%s=%s\n", i.name, i.value if i.value? and i.value 137 | end 138 | } 139 | end 140 | 141 | def load_standard_entries 142 | standard_entries(@rbconfig).each do |ent| 143 | add ent 144 | end 145 | end 146 | 147 | def standard_entries(rbconfig) 148 | c = rbconfig 149 | 150 | rubypath = File.join(c['bindir'], c['ruby_install_name'] + c['EXEEXT']) 151 | 152 | major = c['MAJOR'].to_i 153 | minor = c['MINOR'].to_i 154 | teeny = c['TEENY'].to_i 155 | version = "#{major}.#{minor}" 156 | 157 | # ruby ver. >= 1.4.4? 158 | newpath_p = ((major >= 2) or 159 | ((major == 1) and 160 | ((minor >= 5) or 161 | ((minor == 4) and (teeny >= 4))))) 162 | 163 | if c['rubylibdir'] 164 | # V > 1.6.3 165 | libruby = "#{c['prefix']}/lib/ruby" 166 | librubyver = c['rubylibdir'] 167 | librubyverarch = c['archdir'] 168 | siteruby = c['sitedir'] 169 | siterubyver = c['sitelibdir'] 170 | siterubyverarch = c['sitearchdir'] 171 | elsif newpath_p 172 | # 1.4.4 <= V <= 1.6.3 173 | libruby = "#{c['prefix']}/lib/ruby" 174 | librubyver = "#{c['prefix']}/lib/ruby/#{version}" 175 | librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" 176 | siteruby = c['sitedir'] 177 | siterubyver = "$siteruby/#{version}" 178 | siterubyverarch = "$siterubyver/#{c['arch']}" 179 | else 180 | # V < 1.4.4 181 | libruby = "#{c['prefix']}/lib/ruby" 182 | librubyver = "#{c['prefix']}/lib/ruby/#{version}" 183 | librubyverarch = "#{c['prefix']}/lib/ruby/#{version}/#{c['arch']}" 184 | siteruby = "#{c['prefix']}/lib/ruby/#{version}/site_ruby" 185 | siterubyver = siteruby 186 | siterubyverarch = "$siterubyver/#{c['arch']}" 187 | end 188 | parameterize = lambda {|path| 189 | path.sub(/\A#{Regexp.quote(c['prefix'])}/, '$prefix') 190 | } 191 | 192 | if arg = c['configure_args'].split.detect {|arg| /--with-make-prog=/ =~ arg } 193 | makeprog = arg.sub(/'/, '').split(/=/, 2)[1] 194 | else 195 | makeprog = 'make' 196 | end 197 | 198 | [ 199 | ExecItem.new('installdirs', 'std/site/home', 200 | 'std: install under libruby; site: install under site_ruby; home: install under $HOME')\ 201 | {|val, table| 202 | case val 203 | when 'std' 204 | table['rbdir'] = '$librubyver' 205 | table['sodir'] = '$librubyverarch' 206 | when 'site' 207 | table['rbdir'] = '$siterubyver' 208 | table['sodir'] = '$siterubyverarch' 209 | when 'home' 210 | setup_rb_error '$HOME was not set' unless ENV['HOME'] 211 | table['prefix'] = ENV['HOME'] 212 | table['rbdir'] = '$libdir/ruby' 213 | table['sodir'] = '$libdir/ruby' 214 | end 215 | }, 216 | PathItem.new('prefix', 'path', c['prefix'], 217 | 'path prefix of target environment'), 218 | PathItem.new('bindir', 'path', parameterize.call(c['bindir']), 219 | 'the directory for commands'), 220 | PathItem.new('libdir', 'path', parameterize.call(c['libdir']), 221 | 'the directory for libraries'), 222 | PathItem.new('datadir', 'path', parameterize.call(c['datadir']), 223 | 'the directory for shared data'), 224 | PathItem.new('mandir', 'path', parameterize.call(c['mandir']), 225 | 'the directory for man pages'), 226 | PathItem.new('sysconfdir', 'path', parameterize.call(c['sysconfdir']), 227 | 'the directory for system configuration files'), 228 | PathItem.new('localstatedir', 'path', parameterize.call(c['localstatedir']), 229 | 'the directory for local state data'), 230 | PathItem.new('libruby', 'path', libruby, 231 | 'the directory for ruby libraries'), 232 | PathItem.new('librubyver', 'path', librubyver, 233 | 'the directory for standard ruby libraries'), 234 | PathItem.new('librubyverarch', 'path', librubyverarch, 235 | 'the directory for standard ruby extensions'), 236 | PathItem.new('siteruby', 'path', siteruby, 237 | 'the directory for version-independent aux ruby libraries'), 238 | PathItem.new('siterubyver', 'path', siterubyver, 239 | 'the directory for aux ruby libraries'), 240 | PathItem.new('siterubyverarch', 'path', siterubyverarch, 241 | 'the directory for aux ruby binaries'), 242 | PathItem.new('rbdir', 'path', '$siterubyver', 243 | 'the directory for ruby scripts'), 244 | PathItem.new('sodir', 'path', '$siterubyverarch', 245 | 'the directory for ruby extentions'), 246 | PathItem.new('rubypath', 'path', rubypath, 247 | 'the path to set to #! line'), 248 | ProgramItem.new('rubyprog', 'name', rubypath, 249 | 'the ruby program using for installation'), 250 | ProgramItem.new('makeprog', 'name', makeprog, 251 | 'the make program to compile ruby extentions'), 252 | SelectItem.new('shebang', 'all/ruby/never', 'ruby', 253 | 'shebang line (#!) editing mode'), 254 | BoolItem.new('without-ext', 'yes/no', 'no', 255 | 'does not compile/install ruby extentions') 256 | ] 257 | end 258 | private :standard_entries 259 | 260 | def load_multipackage_entries 261 | multipackage_entries().each do |ent| 262 | add ent 263 | end 264 | end 265 | 266 | def multipackage_entries 267 | [ 268 | PackageSelectionItem.new('with', 'name,name...', '', 'ALL', 269 | 'package names that you want to install'), 270 | PackageSelectionItem.new('without', 'name,name...', '', 'NONE', 271 | 'package names that you do not want to install') 272 | ] 273 | end 274 | private :multipackage_entries 275 | 276 | ALIASES = { 277 | 'std-ruby' => 'librubyver', 278 | 'stdruby' => 'librubyver', 279 | 'rubylibdir' => 'librubyver', 280 | 'archdir' => 'librubyverarch', 281 | 'site-ruby-common' => 'siteruby', # For backward compatibility 282 | 'site-ruby' => 'siterubyver', # For backward compatibility 283 | 'bin-dir' => 'bindir', 284 | 'bin-dir' => 'bindir', 285 | 'rb-dir' => 'rbdir', 286 | 'so-dir' => 'sodir', 287 | 'data-dir' => 'datadir', 288 | 'ruby-path' => 'rubypath', 289 | 'ruby-prog' => 'rubyprog', 290 | 'ruby' => 'rubyprog', 291 | 'make-prog' => 'makeprog', 292 | 'make' => 'makeprog' 293 | } 294 | 295 | def fixup 296 | ALIASES.each do |ali, name| 297 | @table[ali] = @table[name] 298 | end 299 | @items.freeze 300 | @table.freeze 301 | @options_re = /\A--(#{@table.keys.join('|')})(?:=(.*))?\z/ 302 | end 303 | 304 | def parse_opt(opt) 305 | m = @options_re.match(opt) or setup_rb_error "config: unknown option #{opt}" 306 | m.to_a[1,2] 307 | end 308 | 309 | def dllext 310 | @rbconfig['DLEXT'] 311 | end 312 | 313 | def value_config?(name) 314 | lookup(name).value? 315 | end 316 | 317 | class Item 318 | def initialize(name, template, default, desc) 319 | @name = name.freeze 320 | @template = template 321 | @value = default 322 | @default = default 323 | @description = desc 324 | end 325 | 326 | attr_reader :name 327 | attr_reader :description 328 | 329 | attr_accessor :default 330 | alias help_default default 331 | 332 | def help_opt 333 | "--#{@name}=#{@template}" 334 | end 335 | 336 | def value? 337 | true 338 | end 339 | 340 | def value 341 | @value 342 | end 343 | 344 | def resolve(table) 345 | @value.gsub(%r<\$([^/]+)>) { table[$1] } 346 | end 347 | 348 | def set(val) 349 | @value = check(val) 350 | end 351 | 352 | private 353 | 354 | def check(val) 355 | setup_rb_error "config: --#{name} requires argument" unless val 356 | val 357 | end 358 | end 359 | 360 | class BoolItem < Item 361 | def config_type 362 | 'bool' 363 | end 364 | 365 | def help_opt 366 | "--#{@name}" 367 | end 368 | 369 | private 370 | 371 | def check(val) 372 | return 'yes' unless val 373 | case val 374 | when /\Ay(es)?\z/i, /\At(rue)?\z/i then 'yes' 375 | when /\An(o)?\z/i, /\Af(alse)\z/i then 'no' 376 | else 377 | setup_rb_error "config: --#{@name} accepts only yes/no for argument" 378 | end 379 | end 380 | end 381 | 382 | class PathItem < Item 383 | def config_type 384 | 'path' 385 | end 386 | 387 | private 388 | 389 | def check(path) 390 | setup_rb_error "config: --#{@name} requires argument" unless path 391 | path[0,1] == '$' ? path : File.expand_path(path) 392 | end 393 | end 394 | 395 | class ProgramItem < Item 396 | def config_type 397 | 'program' 398 | end 399 | end 400 | 401 | class SelectItem < Item 402 | def initialize(name, selection, default, desc) 403 | super 404 | @ok = selection.split('/') 405 | end 406 | 407 | def config_type 408 | 'select' 409 | end 410 | 411 | private 412 | 413 | def check(val) 414 | unless @ok.include?(val.strip) 415 | setup_rb_error "config: use --#{@name}=#{@template} (#{val})" 416 | end 417 | val.strip 418 | end 419 | end 420 | 421 | class ExecItem < Item 422 | def initialize(name, selection, desc, &block) 423 | super name, selection, nil, desc 424 | @ok = selection.split('/') 425 | @action = block 426 | end 427 | 428 | def config_type 429 | 'exec' 430 | end 431 | 432 | def value? 433 | false 434 | end 435 | 436 | def resolve(table) 437 | setup_rb_error "$#{name()} wrongly used as option value" 438 | end 439 | 440 | undef set 441 | 442 | def evaluate(val, table) 443 | v = val.strip.downcase 444 | unless @ok.include?(v) 445 | setup_rb_error "invalid option --#{@name}=#{val} (use #{@template})" 446 | end 447 | @action.call v, table 448 | end 449 | end 450 | 451 | class PackageSelectionItem < Item 452 | def initialize(name, template, default, help_default, desc) 453 | super name, template, default, desc 454 | @help_default = help_default 455 | end 456 | 457 | attr_reader :help_default 458 | 459 | def config_type 460 | 'package' 461 | end 462 | 463 | private 464 | 465 | def check(val) 466 | unless File.dir?("packages/#{val}") 467 | setup_rb_error "config: no such package: #{val}" 468 | end 469 | val 470 | end 471 | end 472 | 473 | class MetaConfigEnvironment 474 | def initialize(config, installer) 475 | @config = config 476 | @installer = installer 477 | end 478 | 479 | def config_names 480 | @config.names 481 | end 482 | 483 | def config?(name) 484 | @config.key?(name) 485 | end 486 | 487 | def bool_config?(name) 488 | @config.lookup(name).config_type == 'bool' 489 | end 490 | 491 | def path_config?(name) 492 | @config.lookup(name).config_type == 'path' 493 | end 494 | 495 | def value_config?(name) 496 | @config.lookup(name).config_type != 'exec' 497 | end 498 | 499 | def add_config(item) 500 | @config.add item 501 | end 502 | 503 | def add_bool_config(name, default, desc) 504 | @config.add BoolItem.new(name, 'yes/no', default ? 'yes' : 'no', desc) 505 | end 506 | 507 | def add_path_config(name, default, desc) 508 | @config.add PathItem.new(name, 'path', default, desc) 509 | end 510 | 511 | def set_config_default(name, default) 512 | @config.lookup(name).default = default 513 | end 514 | 515 | def remove_config(name) 516 | @config.remove(name) 517 | end 518 | 519 | # For only multipackage 520 | def packages 521 | raise '[setup.rb fatal] multi-package metaconfig API packages() called for single-package; contact application package vendor' unless @installer 522 | @installer.packages 523 | end 524 | 525 | # For only multipackage 526 | def declare_packages(list) 527 | raise '[setup.rb fatal] multi-package metaconfig API declare_packages() called for single-package; contact application package vendor' unless @installer 528 | @installer.packages = list 529 | end 530 | end 531 | 532 | end # class ConfigTable 533 | 534 | 535 | # This module requires: #verbose?, #no_harm? 536 | module FileOperations 537 | 538 | def mkdir_p(dirname, prefix = nil) 539 | dirname = prefix + File.expand_path(dirname) if prefix 540 | $stderr.puts "mkdir -p #{dirname}" if verbose? 541 | return if no_harm? 542 | 543 | # Does not check '/', it's too abnormal. 544 | dirs = File.expand_path(dirname).split(%r<(?=/)>) 545 | if /\A[a-z]:\z/i =~ dirs[0] 546 | disk = dirs.shift 547 | dirs[0] = disk + dirs[0] 548 | end 549 | dirs.each_index do |idx| 550 | path = dirs[0..idx].join('') 551 | Dir.mkdir path unless File.dir?(path) 552 | end 553 | end 554 | 555 | def rm_f(path) 556 | $stderr.puts "rm -f #{path}" if verbose? 557 | return if no_harm? 558 | force_remove_file path 559 | end 560 | 561 | def rm_rf(path) 562 | $stderr.puts "rm -rf #{path}" if verbose? 563 | return if no_harm? 564 | remove_tree path 565 | end 566 | 567 | def remove_tree(path) 568 | if File.symlink?(path) 569 | remove_file path 570 | elsif File.dir?(path) 571 | remove_tree0 path 572 | else 573 | force_remove_file path 574 | end 575 | end 576 | 577 | def remove_tree0(path) 578 | Dir.foreach(path) do |ent| 579 | next if ent == '.' 580 | next if ent == '..' 581 | entpath = "#{path}/#{ent}" 582 | if File.symlink?(entpath) 583 | remove_file entpath 584 | elsif File.dir?(entpath) 585 | remove_tree0 entpath 586 | else 587 | force_remove_file entpath 588 | end 589 | end 590 | begin 591 | Dir.rmdir path 592 | rescue Errno::ENOTEMPTY 593 | # directory may not be empty 594 | end 595 | end 596 | 597 | def move_file(src, dest) 598 | force_remove_file dest 599 | begin 600 | File.rename src, dest 601 | rescue 602 | File.open(dest, 'wb') {|f| 603 | f.write File.binread(src) 604 | } 605 | File.chmod File.stat(src).mode, dest 606 | File.unlink src 607 | end 608 | end 609 | 610 | def force_remove_file(path) 611 | begin 612 | remove_file path 613 | rescue 614 | end 615 | end 616 | 617 | def remove_file(path) 618 | File.chmod 0777, path 619 | File.unlink path 620 | end 621 | 622 | def install(from, dest, mode, prefix = nil) 623 | $stderr.puts "install #{from} #{dest}" if verbose? 624 | return if no_harm? 625 | 626 | realdest = prefix ? prefix + File.expand_path(dest) : dest 627 | realdest = File.join(realdest, File.basename(from)) if File.dir?(realdest) 628 | str = File.binread(from) 629 | if diff?(str, realdest) 630 | verbose_off { 631 | rm_f realdest if File.exist?(realdest) 632 | } 633 | File.open(realdest, 'wb') {|f| 634 | f.write str 635 | } 636 | File.chmod mode, realdest 637 | 638 | File.open("#{objdir_root()}/InstalledFiles", 'a') {|f| 639 | if prefix 640 | f.puts realdest.sub(prefix, '') 641 | else 642 | f.puts realdest 643 | end 644 | } 645 | end 646 | end 647 | 648 | def diff?(new_content, path) 649 | return true unless File.exist?(path) 650 | new_content != File.binread(path) 651 | end 652 | 653 | def command(*args) 654 | $stderr.puts args.join(' ') if verbose? 655 | system(*args) or raise RuntimeError, 656 | "system(#{args.map{|a| a.inspect }.join(' ')}) failed" 657 | end 658 | 659 | def ruby(*args) 660 | command config('rubyprog'), *args 661 | end 662 | 663 | def make(task = nil) 664 | command(*[config('makeprog'), task].compact) 665 | end 666 | 667 | def extdir?(dir) 668 | File.exist?("#{dir}/MANIFEST") or File.exist?("#{dir}/extconf.rb") 669 | end 670 | 671 | def files_of(dir) 672 | Dir.open(dir) {|d| 673 | return d.select {|ent| File.file?("#{dir}/#{ent}") } 674 | } 675 | end 676 | 677 | DIR_REJECT = %w( . .. CVS SCCS RCS CVS.adm .svn ) 678 | 679 | def directories_of(dir) 680 | Dir.open(dir) {|d| 681 | return d.select {|ent| File.dir?("#{dir}/#{ent}") } - DIR_REJECT 682 | } 683 | end 684 | 685 | end 686 | 687 | 688 | # This module requires: #srcdir_root, #objdir_root, #relpath 689 | module HookScriptAPI 690 | 691 | def get_config(key) 692 | @config[key] 693 | end 694 | 695 | alias config get_config 696 | 697 | # obsolete: use metaconfig to change configuration 698 | def set_config(key, val) 699 | @config[key] = val 700 | end 701 | 702 | # 703 | # srcdir/objdir (works only in the package directory) 704 | # 705 | 706 | def curr_srcdir 707 | "#{srcdir_root()}/#{relpath()}" 708 | end 709 | 710 | def curr_objdir 711 | "#{objdir_root()}/#{relpath()}" 712 | end 713 | 714 | def srcfile(path) 715 | "#{curr_srcdir()}/#{path}" 716 | end 717 | 718 | def srcexist?(path) 719 | File.exist?(srcfile(path)) 720 | end 721 | 722 | def srcdirectory?(path) 723 | File.dir?(srcfile(path)) 724 | end 725 | 726 | def srcfile?(path) 727 | File.file?(srcfile(path)) 728 | end 729 | 730 | def srcentries(path = '.') 731 | Dir.open("#{curr_srcdir()}/#{path}") {|d| 732 | return d.to_a - %w(. ..) 733 | } 734 | end 735 | 736 | def srcfiles(path = '.') 737 | srcentries(path).select {|fname| 738 | File.file?(File.join(curr_srcdir(), path, fname)) 739 | } 740 | end 741 | 742 | def srcdirectories(path = '.') 743 | srcentries(path).select {|fname| 744 | File.dir?(File.join(curr_srcdir(), path, fname)) 745 | } 746 | end 747 | 748 | end 749 | 750 | 751 | class ToplevelInstaller 752 | 753 | Version = '3.4.1' 754 | Copyright = 'Copyright (c) 2000-2005 Minero Aoki' 755 | 756 | TASKS = [ 757 | [ 'all', 'do config, setup, then install' ], 758 | [ 'config', 'saves your configurations' ], 759 | [ 'show', 'shows current configuration' ], 760 | [ 'setup', 'compiles ruby extentions and others' ], 761 | [ 'install', 'installs files' ], 762 | [ 'test', 'run all tests in test/' ], 763 | [ 'clean', "does `make clean' for each extention" ], 764 | [ 'distclean',"does `make distclean' for each extention" ] 765 | ] 766 | 767 | def ToplevelInstaller.invoke 768 | config = ConfigTable.new(load_rbconfig()) 769 | config.load_standard_entries 770 | config.load_multipackage_entries if multipackage? 771 | config.fixup 772 | klass = (multipackage?() ? ToplevelInstallerMulti : ToplevelInstaller) 773 | klass.new(File.dirname($0), config).invoke 774 | end 775 | 776 | def ToplevelInstaller.multipackage? 777 | File.dir?(File.dirname($0) + '/packages') 778 | end 779 | 780 | def ToplevelInstaller.load_rbconfig 781 | if arg = ARGV.detect {|arg| /\A--rbconfig=/ =~ arg } 782 | ARGV.delete(arg) 783 | load File.expand_path(arg.split(/=/, 2)[1]) 784 | $".push 'rbconfig.rb' 785 | else 786 | require 'rbconfig' 787 | end 788 | ::Config::CONFIG 789 | end 790 | 791 | def initialize(ardir_root, config) 792 | @ardir = File.expand_path(ardir_root) 793 | @config = config 794 | # cache 795 | @valid_task_re = nil 796 | end 797 | 798 | def config(key) 799 | @config[key] 800 | end 801 | 802 | def inspect 803 | "#<#{self.class} #{__id__()}>" 804 | end 805 | 806 | def invoke 807 | run_metaconfigs 808 | case task = parsearg_global() 809 | when nil, 'all' 810 | parsearg_config 811 | init_installers 812 | exec_config 813 | exec_setup 814 | exec_install 815 | else 816 | case task 817 | when 'config', 'test' 818 | ; 819 | when 'clean', 'distclean' 820 | @config.load_savefile if File.exist?(@config.savefile) 821 | else 822 | @config.load_savefile 823 | end 824 | __send__ "parsearg_#{task}" 825 | init_installers 826 | __send__ "exec_#{task}" 827 | end 828 | end 829 | 830 | def run_metaconfigs 831 | @config.load_script "#{@ardir}/metaconfig" 832 | end 833 | 834 | def init_installers 835 | @installer = Installer.new(@config, @ardir, File.expand_path('.')) 836 | end 837 | 838 | # 839 | # Hook Script API bases 840 | # 841 | 842 | def srcdir_root 843 | @ardir 844 | end 845 | 846 | def objdir_root 847 | '.' 848 | end 849 | 850 | def relpath 851 | '.' 852 | end 853 | 854 | # 855 | # Option Parsing 856 | # 857 | 858 | def parsearg_global 859 | while arg = ARGV.shift 860 | case arg 861 | when /\A\w+\z/ 862 | setup_rb_error "invalid task: #{arg}" unless valid_task?(arg) 863 | return arg 864 | when '-q', '--quiet' 865 | @config.verbose = false 866 | when '--verbose' 867 | @config.verbose = true 868 | when '--help' 869 | print_usage $stdout 870 | exit 0 871 | when '--version' 872 | puts "#{File.basename($0)} version #{Version}" 873 | exit 0 874 | when '--copyright' 875 | puts Copyright 876 | exit 0 877 | else 878 | setup_rb_error "unknown global option '#{arg}'" 879 | end 880 | end 881 | nil 882 | end 883 | 884 | def valid_task?(t) 885 | valid_task_re() =~ t 886 | end 887 | 888 | def valid_task_re 889 | @valid_task_re ||= /\A(?:#{TASKS.map {|task,desc| task }.join('|')})\z/ 890 | end 891 | 892 | def parsearg_no_options 893 | unless ARGV.empty? 894 | task = caller(0).first.slice(%r<`parsearg_(\w+)'>, 1) 895 | setup_rb_error "#{task}: unknown options: #{ARGV.join(' ')}" 896 | end 897 | end 898 | 899 | alias parsearg_show parsearg_no_options 900 | alias parsearg_setup parsearg_no_options 901 | alias parsearg_test parsearg_no_options 902 | alias parsearg_clean parsearg_no_options 903 | alias parsearg_distclean parsearg_no_options 904 | 905 | def parsearg_config 906 | evalopt = [] 907 | set = [] 908 | @config.config_opt = [] 909 | while i = ARGV.shift 910 | if /\A--?\z/ =~ i 911 | @config.config_opt = ARGV.dup 912 | break 913 | end 914 | name, value = *@config.parse_opt(i) 915 | if @config.value_config?(name) 916 | @config[name] = value 917 | else 918 | evalopt.push [name, value] 919 | end 920 | set.push name 921 | end 922 | evalopt.each do |name, value| 923 | @config.lookup(name).evaluate value, @config 924 | end 925 | # Check if configuration is valid 926 | set.each do |n| 927 | @config[n] if @config.value_config?(n) 928 | end 929 | end 930 | 931 | def parsearg_install 932 | @config.no_harm = false 933 | @config.install_prefix = '' 934 | while a = ARGV.shift 935 | case a 936 | when '--no-harm' 937 | @config.no_harm = true 938 | when /\A--prefix=/ 939 | path = a.split(/=/, 2)[1] 940 | path = File.expand_path(path) unless path[0,1] == '/' 941 | @config.install_prefix = path 942 | else 943 | setup_rb_error "install: unknown option #{a}" 944 | end 945 | end 946 | end 947 | 948 | def print_usage(out) 949 | out.puts 'Typical Installation Procedure:' 950 | out.puts " $ ruby #{File.basename $0} config" 951 | out.puts " $ ruby #{File.basename $0} setup" 952 | out.puts " # ruby #{File.basename $0} install (may require root privilege)" 953 | out.puts 954 | out.puts 'Detailed Usage:' 955 | out.puts " ruby #{File.basename $0} " 956 | out.puts " ruby #{File.basename $0} [] []" 957 | 958 | fmt = " %-24s %s\n" 959 | out.puts 960 | out.puts 'Global options:' 961 | out.printf fmt, '-q,--quiet', 'suppress message outputs' 962 | out.printf fmt, ' --verbose', 'output messages verbosely' 963 | out.printf fmt, ' --help', 'print this message' 964 | out.printf fmt, ' --version', 'print version and quit' 965 | out.printf fmt, ' --copyright', 'print copyright and quit' 966 | out.puts 967 | out.puts 'Tasks:' 968 | TASKS.each do |name, desc| 969 | out.printf fmt, name, desc 970 | end 971 | 972 | fmt = " %-24s %s [%s]\n" 973 | out.puts 974 | out.puts 'Options for CONFIG or ALL:' 975 | @config.each do |item| 976 | out.printf fmt, item.help_opt, item.description, item.help_default 977 | end 978 | out.printf fmt, '--rbconfig=path', 'rbconfig.rb to load',"running ruby's" 979 | out.puts 980 | out.puts 'Options for INSTALL:' 981 | out.printf fmt, '--no-harm', 'only display what to do if given', 'off' 982 | out.printf fmt, '--prefix=path', 'install path prefix', '' 983 | out.puts 984 | end 985 | 986 | # 987 | # Task Handlers 988 | # 989 | 990 | def exec_config 991 | @installer.exec_config 992 | @config.save # must be final 993 | end 994 | 995 | def exec_setup 996 | @installer.exec_setup 997 | end 998 | 999 | def exec_install 1000 | @installer.exec_install 1001 | end 1002 | 1003 | def exec_test 1004 | @installer.exec_test 1005 | end 1006 | 1007 | def exec_show 1008 | @config.each do |i| 1009 | printf "%-20s %s\n", i.name, i.value if i.value? 1010 | end 1011 | end 1012 | 1013 | def exec_clean 1014 | @installer.exec_clean 1015 | end 1016 | 1017 | def exec_distclean 1018 | @installer.exec_distclean 1019 | end 1020 | 1021 | end # class ToplevelInstaller 1022 | 1023 | 1024 | class ToplevelInstallerMulti < ToplevelInstaller 1025 | 1026 | include FileOperations 1027 | 1028 | def initialize(ardir_root, config) 1029 | super 1030 | @packages = directories_of("#{@ardir}/packages") 1031 | raise 'no package exists' if @packages.empty? 1032 | @root_installer = Installer.new(@config, @ardir, File.expand_path('.')) 1033 | end 1034 | 1035 | def run_metaconfigs 1036 | @config.load_script "#{@ardir}/metaconfig", self 1037 | @packages.each do |name| 1038 | @config.load_script "#{@ardir}/packages/#{name}/metaconfig" 1039 | end 1040 | end 1041 | 1042 | attr_reader :packages 1043 | 1044 | def packages=(list) 1045 | raise 'package list is empty' if list.empty? 1046 | list.each do |name| 1047 | raise "directory packages/#{name} does not exist"\ 1048 | unless File.dir?("#{@ardir}/packages/#{name}") 1049 | end 1050 | @packages = list 1051 | end 1052 | 1053 | def init_installers 1054 | @installers = {} 1055 | @packages.each do |pack| 1056 | @installers[pack] = Installer.new(@config, 1057 | "#{@ardir}/packages/#{pack}", 1058 | "packages/#{pack}") 1059 | end 1060 | with = extract_selection(config('with')) 1061 | without = extract_selection(config('without')) 1062 | @selected = @installers.keys.select {|name| 1063 | (with.empty? or with.include?(name)) \ 1064 | and not without.include?(name) 1065 | } 1066 | end 1067 | 1068 | def extract_selection(list) 1069 | a = list.split(/,/) 1070 | a.each do |name| 1071 | setup_rb_error "no such package: #{name}" unless @installers.key?(name) 1072 | end 1073 | a 1074 | end 1075 | 1076 | def print_usage(f) 1077 | super 1078 | f.puts 'Inluded packages:' 1079 | f.puts ' ' + @packages.sort.join(' ') 1080 | f.puts 1081 | end 1082 | 1083 | # 1084 | # Task Handlers 1085 | # 1086 | 1087 | def exec_config 1088 | run_hook 'pre-config' 1089 | each_selected_installers {|inst| inst.exec_config } 1090 | run_hook 'post-config' 1091 | @config.save # must be final 1092 | end 1093 | 1094 | def exec_setup 1095 | run_hook 'pre-setup' 1096 | each_selected_installers {|inst| inst.exec_setup } 1097 | run_hook 'post-setup' 1098 | end 1099 | 1100 | def exec_install 1101 | run_hook 'pre-install' 1102 | each_selected_installers {|inst| inst.exec_install } 1103 | run_hook 'post-install' 1104 | end 1105 | 1106 | def exec_test 1107 | run_hook 'pre-test' 1108 | each_selected_installers {|inst| inst.exec_test } 1109 | run_hook 'post-test' 1110 | end 1111 | 1112 | def exec_clean 1113 | rm_f @config.savefile 1114 | run_hook 'pre-clean' 1115 | each_selected_installers {|inst| inst.exec_clean } 1116 | run_hook 'post-clean' 1117 | end 1118 | 1119 | def exec_distclean 1120 | rm_f @config.savefile 1121 | run_hook 'pre-distclean' 1122 | each_selected_installers {|inst| inst.exec_distclean } 1123 | run_hook 'post-distclean' 1124 | end 1125 | 1126 | # 1127 | # lib 1128 | # 1129 | 1130 | def each_selected_installers 1131 | Dir.mkdir 'packages' unless File.dir?('packages') 1132 | @selected.each do |pack| 1133 | $stderr.puts "Processing the package `#{pack}' ..." if verbose? 1134 | Dir.mkdir "packages/#{pack}" unless File.dir?("packages/#{pack}") 1135 | Dir.chdir "packages/#{pack}" 1136 | yield @installers[pack] 1137 | Dir.chdir '../..' 1138 | end 1139 | end 1140 | 1141 | def run_hook(id) 1142 | @root_installer.run_hook id 1143 | end 1144 | 1145 | # module FileOperations requires this 1146 | def verbose? 1147 | @config.verbose? 1148 | end 1149 | 1150 | # module FileOperations requires this 1151 | def no_harm? 1152 | @config.no_harm? 1153 | end 1154 | 1155 | end # class ToplevelInstallerMulti 1156 | 1157 | 1158 | class Installer 1159 | 1160 | FILETYPES = %w( bin lib ext data conf man ) 1161 | 1162 | include FileOperations 1163 | include HookScriptAPI 1164 | 1165 | def initialize(config, srcroot, objroot) 1166 | @config = config 1167 | @srcdir = File.expand_path(srcroot) 1168 | @objdir = File.expand_path(objroot) 1169 | @currdir = '.' 1170 | end 1171 | 1172 | def inspect 1173 | "#<#{self.class} #{File.basename(@srcdir)}>" 1174 | end 1175 | 1176 | def noop(rel) 1177 | end 1178 | 1179 | # 1180 | # Hook Script API base methods 1181 | # 1182 | 1183 | def srcdir_root 1184 | @srcdir 1185 | end 1186 | 1187 | def objdir_root 1188 | @objdir 1189 | end 1190 | 1191 | def relpath 1192 | @currdir 1193 | end 1194 | 1195 | # 1196 | # Config Access 1197 | # 1198 | 1199 | # module FileOperations requires this 1200 | def verbose? 1201 | @config.verbose? 1202 | end 1203 | 1204 | # module FileOperations requires this 1205 | def no_harm? 1206 | @config.no_harm? 1207 | end 1208 | 1209 | def verbose_off 1210 | begin 1211 | save, @config.verbose = @config.verbose?, false 1212 | yield 1213 | ensure 1214 | @config.verbose = save 1215 | end 1216 | end 1217 | 1218 | # 1219 | # TASK config 1220 | # 1221 | 1222 | def exec_config 1223 | exec_task_traverse 'config' 1224 | end 1225 | 1226 | alias config_dir_bin noop 1227 | alias config_dir_lib noop 1228 | 1229 | def config_dir_ext(rel) 1230 | extconf if extdir?(curr_srcdir()) 1231 | end 1232 | 1233 | alias config_dir_data noop 1234 | alias config_dir_conf noop 1235 | alias config_dir_man noop 1236 | 1237 | def extconf 1238 | ruby "#{curr_srcdir()}/extconf.rb", *@config.config_opt 1239 | end 1240 | 1241 | # 1242 | # TASK setup 1243 | # 1244 | 1245 | def exec_setup 1246 | exec_task_traverse 'setup' 1247 | end 1248 | 1249 | def setup_dir_bin(rel) 1250 | files_of(curr_srcdir()).each do |fname| 1251 | update_shebang_line "#{curr_srcdir()}/#{fname}" 1252 | end 1253 | end 1254 | 1255 | alias setup_dir_lib noop 1256 | 1257 | def setup_dir_ext(rel) 1258 | make if extdir?(curr_srcdir()) 1259 | end 1260 | 1261 | alias setup_dir_data noop 1262 | alias setup_dir_conf noop 1263 | alias setup_dir_man noop 1264 | 1265 | def update_shebang_line(path) 1266 | return if no_harm? 1267 | return if config('shebang') == 'never' 1268 | old = Shebang.load(path) 1269 | if old 1270 | $stderr.puts "warning: #{path}: Shebang line includes too many args. It is not portable and your program may not work." if old.args.size > 1 1271 | new = new_shebang(old) 1272 | return if new.to_s == old.to_s 1273 | else 1274 | return unless config('shebang') == 'all' 1275 | new = Shebang.new(config('rubypath')) 1276 | end 1277 | $stderr.puts "updating shebang: #{File.basename(path)}" if verbose? 1278 | open_atomic_writer(path) {|output| 1279 | File.open(path, 'rb') {|f| 1280 | f.gets if old # discard 1281 | output.puts new.to_s 1282 | output.print f.read 1283 | } 1284 | } 1285 | end 1286 | 1287 | def new_shebang(old) 1288 | if /\Aruby/ =~ File.basename(old.cmd) 1289 | Shebang.new(config('rubypath'), old.args) 1290 | elsif File.basename(old.cmd) == 'env' and old.args.first == 'ruby' 1291 | Shebang.new(config('rubypath'), old.args[1..-1]) 1292 | else 1293 | return old unless config('shebang') == 'all' 1294 | Shebang.new(config('rubypath')) 1295 | end 1296 | end 1297 | 1298 | def open_atomic_writer(path, &block) 1299 | tmpfile = File.basename(path) + '.tmp' 1300 | begin 1301 | File.open(tmpfile, 'wb', &block) 1302 | File.rename tmpfile, File.basename(path) 1303 | ensure 1304 | File.unlink tmpfile if File.exist?(tmpfile) 1305 | end 1306 | end 1307 | 1308 | class Shebang 1309 | def Shebang.load(path) 1310 | line = nil 1311 | File.open(path) {|f| 1312 | line = f.gets 1313 | } 1314 | return nil unless /\A#!/ =~ line 1315 | parse(line) 1316 | end 1317 | 1318 | def Shebang.parse(line) 1319 | cmd, *args = *line.strip.sub(/\A\#!/, '').split(' ') 1320 | new(cmd, args) 1321 | end 1322 | 1323 | def initialize(cmd, args = []) 1324 | @cmd = cmd 1325 | @args = args 1326 | end 1327 | 1328 | attr_reader :cmd 1329 | attr_reader :args 1330 | 1331 | def to_s 1332 | "#! #{@cmd}" + (@args.empty? ? '' : " #{@args.join(' ')}") 1333 | end 1334 | end 1335 | 1336 | # 1337 | # TASK install 1338 | # 1339 | 1340 | def exec_install 1341 | rm_f 'InstalledFiles' 1342 | exec_task_traverse 'install' 1343 | end 1344 | 1345 | def install_dir_bin(rel) 1346 | install_files targetfiles(), "#{config('bindir')}/#{rel}", 0755 1347 | end 1348 | 1349 | def install_dir_lib(rel) 1350 | install_files libfiles(), "#{config('rbdir')}/#{rel}", 0644 1351 | end 1352 | 1353 | def install_dir_ext(rel) 1354 | return unless extdir?(curr_srcdir()) 1355 | install_files rubyextentions('.'), 1356 | "#{config('sodir')}/#{File.dirname(rel)}", 1357 | 0555 1358 | end 1359 | 1360 | def install_dir_data(rel) 1361 | install_files targetfiles(), "#{config('datadir')}/#{rel}", 0644 1362 | end 1363 | 1364 | def install_dir_conf(rel) 1365 | # FIXME: should not remove current config files 1366 | # (rename previous file to .old/.org) 1367 | install_files targetfiles(), "#{config('sysconfdir')}/#{rel}", 0644 1368 | end 1369 | 1370 | def install_dir_man(rel) 1371 | install_files targetfiles(), "#{config('mandir')}/#{rel}", 0644 1372 | end 1373 | 1374 | def install_files(list, dest, mode) 1375 | mkdir_p dest, @config.install_prefix 1376 | list.each do |fname| 1377 | install fname, dest, mode, @config.install_prefix 1378 | end 1379 | end 1380 | 1381 | def libfiles 1382 | glob_reject(%w(*.y *.output), targetfiles()) 1383 | end 1384 | 1385 | def rubyextentions(dir) 1386 | ents = glob_select("*.#{@config.dllext}", targetfiles()) 1387 | if ents.empty? 1388 | setup_rb_error "no ruby extention exists: 'ruby #{$0} setup' first" 1389 | end 1390 | ents 1391 | end 1392 | 1393 | def targetfiles 1394 | mapdir(existfiles() - hookfiles()) 1395 | end 1396 | 1397 | def mapdir(ents) 1398 | ents.map {|ent| 1399 | if File.exist?(ent) 1400 | then ent # objdir 1401 | else "#{curr_srcdir()}/#{ent}" # srcdir 1402 | end 1403 | } 1404 | end 1405 | 1406 | # picked up many entries from cvs-1.11.1/src/ignore.c 1407 | JUNK_FILES = %w( 1408 | core RCSLOG tags TAGS .make.state 1409 | .nse_depinfo #* .#* cvslog.* ,* .del-* *.olb 1410 | *~ *.old *.bak *.BAK *.orig *.rej _$* *$ 1411 | 1412 | *.org *.in .* 1413 | ) 1414 | 1415 | def existfiles 1416 | glob_reject(JUNK_FILES, (files_of(curr_srcdir()) | files_of('.'))) 1417 | end 1418 | 1419 | def hookfiles 1420 | %w( pre-%s post-%s pre-%s.rb post-%s.rb ).map {|fmt| 1421 | %w( config setup install clean ).map {|t| sprintf(fmt, t) } 1422 | }.flatten 1423 | end 1424 | 1425 | def glob_select(pat, ents) 1426 | re = globs2re([pat]) 1427 | ents.select {|ent| re =~ ent } 1428 | end 1429 | 1430 | def glob_reject(pats, ents) 1431 | re = globs2re(pats) 1432 | ents.reject {|ent| re =~ ent } 1433 | end 1434 | 1435 | GLOB2REGEX = { 1436 | '.' => '\.', 1437 | '$' => '\$', 1438 | '#' => '\#', 1439 | '*' => '.*' 1440 | } 1441 | 1442 | def globs2re(pats) 1443 | /\A(?:#{ 1444 | pats.map {|pat| pat.gsub(/[\.\$\#\*]/) {|ch| GLOB2REGEX[ch] } }.join('|') 1445 | })\z/ 1446 | end 1447 | 1448 | # 1449 | # TASK test 1450 | # 1451 | 1452 | TESTDIR = 'test' 1453 | 1454 | def exec_test 1455 | unless File.directory?('test') 1456 | $stderr.puts 'no test in this package' if verbose? 1457 | return 1458 | end 1459 | $stderr.puts 'Running tests...' if verbose? 1460 | begin 1461 | require 'test/unit' 1462 | rescue LoadError 1463 | setup_rb_error 'test/unit cannot loaded. You need Ruby 1.8 or later to invoke this task.' 1464 | end 1465 | runner = Test::Unit::AutoRunner.new(true) 1466 | runner.to_run << TESTDIR 1467 | runner.run 1468 | end 1469 | 1470 | # 1471 | # TASK clean 1472 | # 1473 | 1474 | def exec_clean 1475 | exec_task_traverse 'clean' 1476 | rm_f @config.savefile 1477 | rm_f 'InstalledFiles' 1478 | end 1479 | 1480 | alias clean_dir_bin noop 1481 | alias clean_dir_lib noop 1482 | alias clean_dir_data noop 1483 | alias clean_dir_conf noop 1484 | alias clean_dir_man noop 1485 | 1486 | def clean_dir_ext(rel) 1487 | return unless extdir?(curr_srcdir()) 1488 | make 'clean' if File.file?('Makefile') 1489 | end 1490 | 1491 | # 1492 | # TASK distclean 1493 | # 1494 | 1495 | def exec_distclean 1496 | exec_task_traverse 'distclean' 1497 | rm_f @config.savefile 1498 | rm_f 'InstalledFiles' 1499 | end 1500 | 1501 | alias distclean_dir_bin noop 1502 | alias distclean_dir_lib noop 1503 | 1504 | def distclean_dir_ext(rel) 1505 | return unless extdir?(curr_srcdir()) 1506 | make 'distclean' if File.file?('Makefile') 1507 | end 1508 | 1509 | alias distclean_dir_data noop 1510 | alias distclean_dir_conf noop 1511 | alias distclean_dir_man noop 1512 | 1513 | # 1514 | # Traversing 1515 | # 1516 | 1517 | def exec_task_traverse(task) 1518 | run_hook "pre-#{task}" 1519 | FILETYPES.each do |type| 1520 | if type == 'ext' and config('without-ext') == 'yes' 1521 | $stderr.puts 'skipping ext/* by user option' if verbose? 1522 | next 1523 | end 1524 | traverse task, type, "#{task}_dir_#{type}" 1525 | end 1526 | run_hook "post-#{task}" 1527 | end 1528 | 1529 | def traverse(task, rel, mid) 1530 | dive_into(rel) { 1531 | run_hook "pre-#{task}" 1532 | __send__ mid, rel.sub(%r[\A.*?(?:/|\z)], '') 1533 | directories_of(curr_srcdir()).each do |d| 1534 | traverse task, "#{rel}/#{d}", mid 1535 | end 1536 | run_hook "post-#{task}" 1537 | } 1538 | end 1539 | 1540 | def dive_into(rel) 1541 | return unless File.dir?("#{@srcdir}/#{rel}") 1542 | 1543 | dir = File.basename(rel) 1544 | Dir.mkdir dir unless File.dir?(dir) 1545 | prevdir = Dir.pwd 1546 | Dir.chdir dir 1547 | $stderr.puts '---> ' + rel if verbose? 1548 | @currdir = rel 1549 | yield 1550 | Dir.chdir prevdir 1551 | $stderr.puts '<--- ' + rel if verbose? 1552 | @currdir = File.dirname(rel) 1553 | end 1554 | 1555 | def run_hook(id) 1556 | path = [ "#{curr_srcdir()}/#{id}", 1557 | "#{curr_srcdir()}/#{id}.rb" ].detect {|cand| File.file?(cand) } 1558 | return unless path 1559 | begin 1560 | instance_eval File.read(path), path, 1 1561 | rescue 1562 | raise if $DEBUG 1563 | setup_rb_error "hook #{path} failed:\n" + $!.message 1564 | end 1565 | end 1566 | 1567 | end # class Installer 1568 | 1569 | 1570 | class SetupError < StandardError; end 1571 | 1572 | def setup_rb_error(msg) 1573 | raise SetupError, msg 1574 | end 1575 | 1576 | if $0 == __FILE__ 1577 | begin 1578 | ToplevelInstaller.invoke 1579 | rescue SetupError 1580 | raise if $DEBUG 1581 | $stderr.puts $!.message 1582 | $stderr.puts "Try 'ruby #{$0} --help' for detailed usage." 1583 | exit 1 1584 | end 1585 | end 1586 | -------------------------------------------------------------------------------- /test/test_mysql.rb: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | require 'test/unit' 3 | require 'test/unit/rr' 4 | begin 5 | require 'test/unit/notify' 6 | rescue LoadError 7 | # ignore 8 | end 9 | 10 | require 'mysql' 11 | 12 | # MYSQL_USER must have ALL privilege for MYSQL_DATABASE.* and RELOAD privilege for *.* 13 | MYSQL_SERVER = ENV['MYSQL_SERVER'] 14 | MYSQL_USER = ENV['MYSQL_USER'] 15 | MYSQL_PASSWORD = ENV['MYSQL_PASSWORD'] 16 | MYSQL_DATABASE = ENV['MYSQL_DATABASE'] || "test_for_mysql_ruby" 17 | MYSQL_PORT = ENV['MYSQL_PORT'] 18 | MYSQL_SOCKET = ENV['MYSQL_SOCKET'] 19 | 20 | class TestMysql < Test::Unit::TestCase 21 | sub_test_case 'Mysql::VERSION' do 22 | test 'returns client version' do 23 | assert{ Mysql::VERSION == '3.0.1' } 24 | end 25 | end 26 | 27 | sub_test_case 'Mysql.new' do 28 | test 'returns Mysql object' do 29 | assert{ Mysql.new.kind_of? Mysql } 30 | end 31 | end 32 | 33 | sub_test_case 'arguments' do 34 | test 'with fixed arguments' do 35 | @m = Mysql.new('127.0.0.1', 'hoge', 'abc&def', 'test', 3306, '/tmp/socket', 12345) 36 | assert{ @m.host == '127.0.0.1' } 37 | assert{ @m.username == 'hoge' } 38 | assert{ @m.password == 'abc&def' } 39 | assert{ @m.database == 'test' } 40 | assert{ @m.port == 3306 } 41 | assert{ @m.socket == '/tmp/socket' } 42 | assert{ @m.flags == 12345 } 43 | end 44 | 45 | test 'with keyword arguments' do 46 | @m = Mysql.new(host: '127.0.0.1', username: 'hoge', password: 'abc&def', database: 'test', port: 3306, socket: '/tmp/socket', flags: 12345) 47 | assert{ @m.host == '127.0.0.1' } 48 | assert{ @m.username == 'hoge' } 49 | assert{ @m.password == 'abc&def' } 50 | assert{ @m.database == 'test' } 51 | assert{ @m.port == 3306 } 52 | assert{ @m.socket == '/tmp/socket' } 53 | assert{ @m.flags == 12345 } 54 | end 55 | 56 | test 'with URI' do 57 | uri = URI.parse("mysql://hoge:abc%26def@127.0.0.1:3306/test?socket=/tmp/socket&flags=12345") 58 | @m = Mysql.new(uri) 59 | assert{ @m.host == '127.0.0.1' } 60 | assert{ @m.username == 'hoge' } 61 | assert{ @m.password == 'abc&def' } 62 | assert{ @m.database == 'test' } 63 | assert{ @m.port == 3306 } 64 | assert{ @m.socket == '/tmp/socket' } 65 | assert{ @m.flags == 12345 } 66 | end 67 | 68 | test 'with URI string' do 69 | @m = Mysql.new("mysql://hoge:abc%26def@127.0.0.1:3306/test?socket=/tmp/socket&flags=12345") 70 | assert{ @m.host == '127.0.0.1' } 71 | assert{ @m.username == 'hoge' } 72 | assert{ @m.password == 'abc&def' } 73 | assert{ @m.database == 'test' } 74 | assert{ @m.port == 3306 } 75 | assert{ @m.socket == '/tmp/socket' } 76 | assert{ @m.flags == 12345 } 77 | end 78 | 79 | test 'with URI string: host is filename' do 80 | @m = Mysql.new("mysql://hoge:abc%26def@%2Ftmp%2Fsocket:3306/test?flags=12345") 81 | assert{ @m.host == '' } 82 | assert{ @m.username == 'hoge' } 83 | assert{ @m.password == 'abc&def' } 84 | assert{ @m.database == 'test' } 85 | assert{ @m.port == 3306 } 86 | assert{ @m.socket == '/tmp/socket' } 87 | assert{ @m.flags == 12345 } 88 | end 89 | 90 | teardown do 91 | @m.close if @m 92 | end 93 | end 94 | 95 | sub_test_case 'Mysql.connect' do 96 | test 'connect to mysqld' do 97 | @m = Mysql.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 98 | assert{ @m.kind_of? Mysql } 99 | end 100 | 101 | test 'flag argument affects' do 102 | @m = Mysql.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET, Mysql::CLIENT_FOUND_ROWS) 103 | @m.query 'create temporary table t (c int)' 104 | @m.query 'insert into t values (123)' 105 | @m.query 'update t set c=123' 106 | assert{ @m.affected_rows == 1 } 107 | end 108 | 109 | teardown do 110 | @m.close if @m 111 | end 112 | end 113 | 114 | sub_test_case 'Mysql.escape_string' do 115 | test 'escape special character' do 116 | assert{ Mysql.escape_string("abc'def\"ghi\0jkl%mno") == "abc\\'def\\\"ghi\\0jkl%mno" } 117 | end 118 | end 119 | 120 | sub_test_case 'Mysql.quote' do 121 | test 'escape special character' do 122 | assert{ Mysql.quote("abc'def\"ghi\0jkl%mno") == "abc\\'def\\\"ghi\\0jkl%mno" } 123 | end 124 | end 125 | 126 | sub_test_case 'Mysql#connect' do 127 | test 'connect to mysqld' do 128 | @m = Mysql.new(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 129 | assert{ @m.connect == @m } 130 | end 131 | 132 | test 'connect to mysqld by URI' do 133 | @m = Mysql.new("mysql://#{MYSQL_USER}:#{MYSQL_PASSWORD}@#{MYSQL_SERVER}:#{MYSQL_PORT}/#{MYSQL_DATABASE}?socket=#{MYSQL_SOCKET}") 134 | assert{ @m.connect == @m } 135 | end 136 | 137 | test 'overrides arguments of new method' do 138 | @m = Mysql.new('example.com', 12345) 139 | @m.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 140 | end 141 | 142 | teardown do 143 | @m.close if @m 144 | end 145 | end 146 | 147 | sub_test_case 'options' do 148 | setup do 149 | @m = Mysql.new 150 | end 151 | teardown do 152 | @m.close 153 | end 154 | test 'init_command: execute query when connecting' do 155 | @m.init_command = "SET AUTOCOMMIT=0" 156 | assert{ @m.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) == @m } 157 | assert{ @m.query('select @@AUTOCOMMIT').fetch_row == ["0"] } 158 | end 159 | test 'connect_timeout: set timeout for connecting' do 160 | @m.connect_timeout = 0.1 161 | stub(Socket).tcp{ raise Errno::ETIMEDOUT } 162 | stub(Socket).unix{ raise Errno::ETIMEDOUT } 163 | assert_raise Mysql::ClientError, 'connection timeout' do 164 | @m.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 165 | end 166 | assert_raise Mysql::ClientError, 'connection timeout' do 167 | @m.connect 168 | end 169 | end 170 | test 'local_infile: client can execute LOAD DATA LOCAL INFILE query' do 171 | require 'tempfile' 172 | tmpf = Tempfile.new 'mysql_spec' 173 | tmpf.puts "123\tabc\n" 174 | tmpf.close 175 | @m.local_infile = true 176 | @m.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 177 | if @m.query('select @@local_infile').fetch[0] == '0' 178 | omit 'skip because local_infile variable is false' 179 | end 180 | @m.query('create temporary table t (i int, c char(10))') 181 | @m.query("load data local infile '#{tmpf.path}' into table t") 182 | assert{ @m.info == 'Records: 1 Deleted: 0 Skipped: 0 Warnings: 0' } 183 | assert{ @m.query('select * from t').fetch_row == ['123','abc'] } 184 | end 185 | test 'load_data_local_dir: client can execute LOAD DATA LOCAL INFILE query with specified directory' do 186 | require 'tempfile' 187 | tmpf = Tempfile.new 'mysql_spec' 188 | tmpf.puts "123\tabc\n" 189 | tmpf.close 190 | @m.load_data_local_dir = File.dirname(tmpf.path) 191 | @m.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 192 | if @m.query('select @@local_infile').fetch[0] == '0' 193 | omit 'skip because local_infile variable is false' 194 | end 195 | @m.query('create temporary table t (i int, c char(10))') 196 | @m.query("load data local infile '#{tmpf.path}' into table t") 197 | assert{ @m.query('select * from t').fetch_row == ['123','abc'] } 198 | end 199 | test 'load_data_local_dir: client cannot execute LOAD DATA LOCAL INFILE query without specified directory' do 200 | require 'tempfile' 201 | tmpf = Tempfile.new 'mysql_spec' 202 | tmpf.puts "123\tabc\n" 203 | tmpf.close 204 | @m.load_data_local_dir = '/hoge' 205 | @m.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 206 | if @m.query('select @@local_infile').fetch[0] == '0' 207 | omit 'skip because local_infile variable is false' 208 | end 209 | @m.query('create temporary table t (i int, c char(10))') 210 | assert_raise Mysql::ClientError::LoadDataLocalInfileRejected, 'LOAD DATA LOCAL INFILE file request rejected due to restrictions on access.' do 211 | @m.query("load data local infile '#{tmpf.path}' into table t") 212 | end 213 | end 214 | test 'without local_infile and load_data_local_dir: client cannot execute LOAD DATA LOCAL INFILE query' do 215 | require 'tempfile' 216 | tmpf = Tempfile.new 'mysql_spec' 217 | tmpf.puts "123\tabc\n" 218 | tmpf.close 219 | @m.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 220 | if @m.query('select @@local_infile').fetch[0] == '0' 221 | omit 'skip because local_infile variable is false' 222 | end 223 | @m.query('create temporary table t (i int, c char(10))') 224 | if @m.server_version >= 80000 225 | assert_raise Mysql::ServerError, 'Loading local data is disabled; this must be enabled on both the client and server sides' do 226 | @m.query("load data local infile '#{tmpf.path}' into table t") 227 | end 228 | else 229 | assert_raise Mysql::ServerError::NotAllowedCommand, 'The used command is not allowed with this MySQL version' do 230 | @m.query("load data local infile '#{tmpf.path}' into table t") 231 | end 232 | end 233 | end 234 | test 'read_timeout: set timeout for reading packet' do 235 | @m.read_timeout = 1 236 | @m.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 237 | @m.query("select 123").entries 238 | end 239 | test 'write_timeout: set timeout for writing packet' do 240 | @m.write_timeout = 1 241 | @m.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 242 | @m.query("select 123").entries 243 | end 244 | test 'charset: set charset for connection' do 245 | @m.charset = 'utf8mb3' 246 | @m.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 247 | assert do 248 | @m.query('select @@character_set_connection').fetch_row == ['utf8mb3'] || 249 | @m.query('select @@character_set_connection').fetch_row == ['utf8'] 250 | end 251 | end 252 | end 253 | 254 | sub_test_case 'Mysql' do 255 | setup do 256 | @m = Mysql.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 257 | end 258 | 259 | teardown do 260 | @m.close if @m rescue nil 261 | end 262 | 263 | sub_test_case '#escape_string' do 264 | test 'escape special character for charset' do 265 | @m.charset = 'cp932' 266 | assert{ @m.escape_string("abc'def\"ghi\0jkl%mno_表".encode('cp932')) == "abc\\'def\\\"ghi\\0jkl%mno_表".encode('cp932') } 267 | end 268 | end 269 | 270 | sub_test_case '#quote' do 271 | test 'is alias of #escape_string' do 272 | assert{ @m.method(:quote) == @m.method(:escape_string) } 273 | end 274 | end 275 | 276 | sub_test_case '#affected_rows' do 277 | test 'returns number of affected rows' do 278 | @m.query 'create temporary table t (id int)' 279 | @m.query 'insert into t values (1),(2)' 280 | assert{ @m.affected_rows == 2 } 281 | end 282 | end 283 | 284 | sub_test_case '#character_set_name' do 285 | test 'returns charset name' do 286 | m = Mysql.new 287 | m.charset = 'cp932' 288 | m.connect MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET 289 | assert{ m.character_set_name == 'cp932' } 290 | end 291 | end 292 | 293 | sub_test_case '#close' do 294 | test 'returns self' do 295 | assert{ @m.close == @m } 296 | end 297 | end 298 | 299 | sub_test_case '#close!' do 300 | test 'returns self' do 301 | assert{ @m.close! == @m } 302 | end 303 | end 304 | 305 | # sub_test_case '#create_db' do 306 | # end 307 | 308 | # sub_test_case '#drop_db' do 309 | # end 310 | 311 | sub_test_case '#errno' do 312 | test 'default value is 0' do 313 | assert{ @m.errno == 0 } 314 | end 315 | test 'returns error number of latest error' do 316 | @m.query('hogehoge') rescue nil 317 | assert{ @m.errno == 1064 } 318 | end 319 | end 320 | 321 | sub_test_case '#error' do 322 | test 'returns error message of latest error' do 323 | @m.query('hogehoge') rescue nil 324 | assert{ @m.error == "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'hogehoge' at line 1" } 325 | end 326 | end 327 | 328 | sub_test_case '#field_count' do 329 | test 'returns number of fields for latest query' do 330 | @m.query 'select 1,2,3' 331 | assert{ @m.field_count == 3 } 332 | end 333 | end 334 | 335 | sub_test_case '#host_info' do 336 | test 'returns connection type as String' do 337 | if MYSQL_SERVER == nil or MYSQL_SERVER == 'localhost' 338 | assert{ @m.host_info == 'Localhost via UNIX socket' } 339 | else 340 | assert{ @m.host_info == "#{MYSQL_SERVER} via TCP/IP" } 341 | end 342 | end 343 | end 344 | 345 | sub_test_case '#server_info' do 346 | test 'returns server version as String' do 347 | assert{ @m.server_info =~ /\A\d+\.\d+\.\d+/ } 348 | end 349 | end 350 | 351 | sub_test_case '#info' do 352 | test 'returns information of latest query' do 353 | @m.query 'create temporary table t (id int)' 354 | @m.query 'insert into t values (1),(2),(3)' 355 | assert{ @m.info == 'Records: 3 Duplicates: 0 Warnings: 0' } 356 | end 357 | end 358 | 359 | sub_test_case '#insert_id' do 360 | test 'returns latest auto_increment value' do 361 | @m.query 'create temporary table t (id int auto_increment, unique (id))' 362 | @m.query 'insert into t values (0)' 363 | assert{ @m.insert_id == 1 } 364 | @m.query 'alter table t auto_increment=1234' 365 | @m.query 'insert into t values (0)' 366 | assert{ @m.insert_id == 1234 } 367 | end 368 | end 369 | 370 | sub_test_case '#kill' do 371 | setup do 372 | @m2 = Mysql.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 373 | end 374 | teardown do 375 | @m2.close rescue nil 376 | end 377 | test 'returns self' do 378 | assert{ @m.kill(@m2.thread_id) == @m } 379 | end 380 | end 381 | 382 | sub_test_case '#ping' do 383 | test 'returns self' do 384 | assert{ @m.ping == @m } 385 | end 386 | end 387 | 388 | sub_test_case '#query' do 389 | test 'returns Mysql::Result if query returns results' do 390 | assert{ @m.query('select 123').kind_of? Mysql::Result } 391 | end 392 | test 'returns nil if query returns no results' do 393 | assert{ @m.query('set @hoge:=123') == nil } 394 | end 395 | test 'returns self if block is specified' do 396 | assert{ @m.query('select 123'){} == @m } 397 | end 398 | end 399 | 400 | sub_test_case '#refresh' do 401 | test 'returns self' do 402 | assert{ @m.refresh(Mysql::REFRESH_HOSTS) == @m } 403 | end 404 | end 405 | 406 | sub_test_case '#reload' do 407 | test 'returns self' do 408 | assert{ @m.reload == @m } 409 | end 410 | end 411 | 412 | sub_test_case '#select_db' do 413 | test 'changes default database' do 414 | @m.select_db 'information_schema' 415 | assert{ @m.query('select database()').fetch_row.first == 'information_schema' } 416 | end 417 | end 418 | 419 | # sub_test_case '#shutdown' do 420 | # end 421 | 422 | sub_test_case '#stat' do 423 | test 'returns server status' do 424 | assert{ @m.stat =~ /\AUptime: \d+ Threads: \d+ Questions: \d+ Slow queries: \d+ Opens: \d+ Flush tables: \d+ Open tables: \d+ Queries per second avg: \d+\.\d+\z/ } 425 | end 426 | end 427 | 428 | sub_test_case '#thread_id' do 429 | test 'returns thread id as Integer' do 430 | assert{ @m.thread_id.kind_of? Integer } 431 | end 432 | end 433 | 434 | sub_test_case '#server_version' do 435 | test 'returns server version as Integer' do 436 | assert{ @m.server_version.kind_of? Integer } 437 | end 438 | end 439 | 440 | sub_test_case '#warning_count' do 441 | setup do 442 | @m.query("set sql_mode=''") 443 | @m.query("set sql_mode=''") # clear warnings on previous `set' statement. 444 | end 445 | test 'default values is zero' do 446 | assert{ @m.warning_count == 0 } 447 | end 448 | test 'returns number of warnings' do 449 | @m.query 'create temporary table t (i tinyint)' 450 | @m.query 'insert into t values (1234567)' 451 | assert{ @m.warning_count == 1 } 452 | end 453 | end 454 | 455 | sub_test_case '#commit' do 456 | test 'returns self' do 457 | assert{ @m.commit == @m } 458 | end 459 | end 460 | 461 | sub_test_case '#rollback' do 462 | test 'returns self' do 463 | assert{ @m.rollback == @m } 464 | end 465 | end 466 | 467 | sub_test_case '#autocommit' do 468 | test 'returns self' do 469 | assert{ @m.autocommit(true) == @m } 470 | end 471 | 472 | test 'change auto-commit mode' do 473 | @m.autocommit(true) 474 | assert{ @m.query('select @@autocommit').fetch_row == ['1'] } 475 | @m.autocommit(false) 476 | assert{ @m.query('select @@autocommit').fetch_row == ['0'] } 477 | end 478 | end 479 | 480 | sub_test_case '#set_server_option' do 481 | test 'returns self' do 482 | assert{ @m.set_server_option(Mysql::OPTION_MULTI_STATEMENTS_ON) == @m } 483 | end 484 | end 485 | 486 | sub_test_case '#sqlstate' do 487 | test 'default values is "00000"' do 488 | assert{ @m.sqlstate == "00000" } 489 | end 490 | test 'returns sqlstate code' do 491 | assert_raise do 492 | @m.query("hoge") 493 | end 494 | assert{ @m.sqlstate == "42000" } 495 | end 496 | end 497 | 498 | sub_test_case '#query with block' do 499 | test 'returns self' do 500 | assert{ @m.query('select 1'){} == @m } 501 | end 502 | test 'evaluate block with Mysql::Result' do 503 | assert{ @m.query('select 1'){|res| res.kind_of? Mysql::Result} == @m } 504 | end 505 | test 'evaluate block multiple times if multiple query is specified' do 506 | @m.set_server_option Mysql::OPTION_MULTI_STATEMENTS_ON 507 | cnt = 0 508 | expect = [["1"], ["2"]] 509 | assert{ @m.query('select 1; select 2'){|res| 510 | assert{ res.fetch_row == expect.shift } 511 | cnt += 1 512 | } == @m } 513 | assert{ cnt == 2 } 514 | end 515 | test 'evaluate block only when query has result' do 516 | @m.set_server_option Mysql::OPTION_MULTI_STATEMENTS_ON 517 | cnt = 0 518 | expect = [["1"], ["2"]] 519 | assert{ @m.query('select 1; set @hoge:=1; select 2'){|res| 520 | assert{ res.fetch_row == expect.shift } 521 | cnt += 1 522 | } == @m } 523 | assert{ cnt == 2 } 524 | end 525 | end 526 | end 527 | 528 | sub_test_case 'multiple statement query:' do 529 | setup do 530 | @m = Mysql.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 531 | @m.set_server_option(Mysql::OPTION_MULTI_STATEMENTS_ON) 532 | @res = @m.query 'select 1,2; select 3,4,5' 533 | end 534 | test 'Mysql#query returns results for first query' do 535 | assert{ @res.entries == [['1','2']] } 536 | end 537 | test 'Mysql#more_results is true' do 538 | assert{ @m.more_results == true } 539 | end 540 | test 'Mysql#more_results? is true' do 541 | assert{ @m.more_results? == true } 542 | end 543 | test 'Mysql#next_result is true' do 544 | assert{ @m.next_result == true } 545 | end 546 | sub_test_case 'for next query:' do 547 | setup do 548 | @m.next_result 549 | @res = @m.store_result 550 | end 551 | test 'Mysql#store_result returns results' do 552 | assert{ @res.entries == [['3','4','5']] } 553 | end 554 | test 'Mysql#more_results is false' do 555 | assert{ @m.more_results == false } 556 | end 557 | test 'Mysql#more_results? is false' do 558 | assert{ @m.more_results? == false } 559 | end 560 | test 'Mysql#next_result is false' do 561 | assert{ @m.next_result == false } 562 | end 563 | end 564 | end 565 | 566 | test 'multiple statement error' do 567 | m = Mysql.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 568 | m.set_server_option(Mysql::OPTION_MULTI_STATEMENTS_ON) 569 | res = m.query 'select 1; select hoge; select 2' 570 | assert{ res.entries == [['1']] } 571 | assert{ m.more_results? == true } 572 | assert_raise(Mysql::ServerError::BadFieldError){ m.next_result } 573 | assert{ m.more_results? == false } 574 | end 575 | 576 | test 'procedure returns multiple results' do 577 | m = Mysql.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 578 | m.query 'drop procedure if exists test_proc' 579 | m.query 'create procedure test_proc() begin select 1 as a; select 2 as b; end' 580 | res = m.query 'call test_proc()' 581 | assert{ res.entries == [['1']] } 582 | assert{ m.more_results? == true } 583 | assert{ m.next_result == true } 584 | assert{ m.store_result.entries == [['2']] } 585 | assert{ m.more_results? == true } 586 | assert{ m.next_result == true } 587 | assert{ m.more_results? == false } 588 | end 589 | 590 | sub_test_case 'Mysql::Result' do 591 | setup do 592 | @m = Mysql.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 593 | @m.charset = 'latin1' 594 | @m.query 'create temporary table t (id int default 0, str char(10), primary key (id))' 595 | @m.query "insert into t values (1,'abc'),(2,'defg'),(3,'hi'),(4,null)" 596 | @res = @m.query 'select * from t' 597 | end 598 | 599 | teardown do 600 | @m.close if @m 601 | end 602 | 603 | test '#data_seek set position of current record' do 604 | assert{ @res.fetch_row == ['1', 'abc'] } 605 | assert{ @res.fetch_row == ['2', 'defg'] } 606 | assert{ @res.fetch_row == ['3', 'hi'] } 607 | @res.data_seek 1 608 | assert{ @res.fetch_row == ['2', 'defg'] } 609 | end 610 | 611 | test '#fetch_field return current field' do 612 | f = @res.fetch_field 613 | assert{ f.name == 'id' } 614 | assert{ f.table == 't' } 615 | assert{ f.def == nil } 616 | assert{ f.type == Mysql::Field::TYPE_LONG } 617 | assert{ f.length == 11 } 618 | assert{ f.max_length == 1 } 619 | assert{ f.flags == Mysql::Field::NUM_FLAG|Mysql::Field::PRI_KEY_FLAG|Mysql::Field::PART_KEY_FLAG|Mysql::Field::NOT_NULL_FLAG } 620 | assert{ f.decimals == 0 } 621 | 622 | f = @res.fetch_field 623 | assert{ f.name == 'str' } 624 | assert{ f.table == 't' } 625 | assert{ f.def == nil } 626 | assert{ f.type == Mysql::Field::TYPE_STRING } 627 | assert{ f.length == 10 } 628 | assert{ f.max_length == 4 } 629 | assert{ f.flags == 0 } 630 | assert{ f.decimals == 0 } 631 | 632 | assert{ @res.fetch_field == nil } 633 | end 634 | 635 | test '#fetch_fields returns array of fields' do 636 | ret = @res.fetch_fields 637 | assert{ ret.size == 2 } 638 | assert{ ret[0].name == 'id' } 639 | assert{ ret[1].name == 'str' } 640 | end 641 | 642 | test '#fetch_field_direct returns field' do 643 | f = @res.fetch_field_direct 0 644 | assert{ f.name == 'id' } 645 | f = @res.fetch_field_direct 1 646 | assert{ f.name == 'str' } 647 | assert_raise Mysql::ClientError, 'invalid argument: -1' do 648 | @res.fetch_field_direct(-1) 649 | end 650 | assert_raise Mysql::ClientError, 'invalid argument: 2' do 651 | @res.fetch_field_direct 2 652 | end 653 | end 654 | 655 | test '#fetch_lengths returns array of length of field data' do 656 | assert{ @res.fetch_lengths == nil } 657 | @res.fetch_row 658 | assert{ @res.fetch_lengths == [1, 3] } 659 | @res.fetch_row 660 | assert{ @res.fetch_lengths == [1, 4] } 661 | @res.fetch_row 662 | assert{ @res.fetch_lengths == [1, 2] } 663 | @res.fetch_row 664 | assert{ @res.fetch_lengths == [1, 0] } 665 | @res.fetch_row 666 | assert{ @res.fetch_lengths == nil } 667 | end 668 | 669 | test '#fetch_row returns one record as array for current record' do 670 | assert{ @res.fetch_row == ['1', 'abc'] } 671 | assert{ @res.fetch_row == ['2', 'defg'] } 672 | assert{ @res.fetch_row == ['3', 'hi'] } 673 | assert{ @res.fetch_row == ['4', nil] } 674 | assert{ @res.fetch_row == nil } 675 | end 676 | 677 | test '#fetch_hash returns one record as hash for current record' do 678 | assert{ @res.fetch_hash == {'id'=>'1', 'str'=>'abc'} } 679 | assert{ @res.fetch_hash == {'id'=>'2', 'str'=>'defg'} } 680 | assert{ @res.fetch_hash == {'id'=>'3', 'str'=>'hi'} } 681 | assert{ @res.fetch_hash == {'id'=>'4', 'str'=>nil} } 682 | assert{ @res.fetch_hash == nil } 683 | end 684 | 685 | test '#fetch_hash(true) returns with table name' do 686 | assert{ @res.fetch_hash(true) == {'t.id'=>'1', 't.str'=>'abc'} } 687 | assert{ @res.fetch_hash(true) == {'t.id'=>'2', 't.str'=>'defg'} } 688 | assert{ @res.fetch_hash(true) == {'t.id'=>'3', 't.str'=>'hi'} } 689 | assert{ @res.fetch_hash(true) == {'t.id'=>'4', 't.str'=>nil} } 690 | assert{ @res.fetch_hash(true) == nil } 691 | end 692 | 693 | test '#num_fields returns number of fields' do 694 | assert{ @res.num_fields == 2 } 695 | end 696 | 697 | test '#num_rows returns number of records' do 698 | assert{ @res.num_rows == 4 } 699 | end 700 | 701 | test '#each iterate block with a record' do 702 | expect = [["1","abc"], ["2","defg"], ["3","hi"], ["4",nil]] 703 | @res.each do |a| 704 | assert{ a == expect.shift } 705 | end 706 | end 707 | 708 | test '#each_hash iterate block with a hash' do 709 | expect = [{"id"=>"1","str"=>"abc"}, {"id"=>"2","str"=>"defg"}, {"id"=>"3","str"=>"hi"}, {"id"=>"4","str"=>nil}] 710 | @res.each_hash do |a| 711 | assert{ a == expect.shift } 712 | end 713 | end 714 | 715 | test '#each_hash(true): hash key has table name' do 716 | expect = [{"t.id"=>"1","t.str"=>"abc"}, {"t.id"=>"2","t.str"=>"defg"}, {"t.id"=>"3","t.str"=>"hi"}, {"t.id"=>"4","t.str"=>nil}] 717 | @res.each_hash(true) do |a| 718 | assert{ a == expect.shift } 719 | end 720 | end 721 | 722 | test '#row_tell returns position of current record, #row_seek set position of current record' do 723 | assert{ @res.fetch_row == ['1', 'abc'] } 724 | pos = @res.row_tell 725 | assert{ @res.fetch_row == ['2', 'defg'] } 726 | assert{ @res.fetch_row == ['3', 'hi'] } 727 | @res.row_seek pos 728 | assert{ @res.fetch_row == ['2', 'defg'] } 729 | end 730 | 731 | test '#field_tell returns position of current field, #field_seek set position of current field' do 732 | assert{ @res.field_tell == 0 } 733 | @res.fetch_field 734 | assert{ @res.field_tell == 1 } 735 | @res.fetch_field 736 | assert{ @res.field_tell == 2 } 737 | @res.field_seek 1 738 | assert{ @res.field_tell == 1 } 739 | end 740 | 741 | test '#free returns nil' do 742 | assert{ @res.free == nil } 743 | end 744 | end 745 | 746 | sub_test_case 'Mysql::Field' do 747 | setup do 748 | @m = Mysql.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 749 | @m.charset = 'latin1' 750 | @m.query 'create temporary table t (id int default 0, str char(10), primary key (id))' 751 | @m.query "insert into t values (1,'abc'),(2,'defg'),(3,'hi'),(4,null)" 752 | @res = @m.query 'select * from t' 753 | end 754 | 755 | teardown do 756 | @m.close if @m 757 | end 758 | 759 | test '#name is name of field' do 760 | assert{ @res.fetch_field.name == 'id' } 761 | end 762 | 763 | test '#table is name of table for field' do 764 | assert{ @res.fetch_field.table == 't' } 765 | end 766 | 767 | test '#def for result set is null' do 768 | assert{ @res.fetch_field.def == nil } 769 | end 770 | 771 | test '#type is type of field as Integer' do 772 | assert{ @res.fetch_field.type == Mysql::Field::TYPE_LONG } 773 | assert{ @res.fetch_field.type == Mysql::Field::TYPE_STRING } 774 | end 775 | 776 | test '#length is length of field' do 777 | assert{ @res.fetch_field.length == 11 } 778 | assert{ @res.fetch_field.length == 10 } 779 | end 780 | 781 | test '#max_length is maximum length of field value' do 782 | assert{ @res.fetch_field.max_length == 1 } 783 | assert{ @res.fetch_field.max_length == 4 } 784 | end 785 | 786 | test '#flags is flag of field as Integer' do 787 | assert{ @res.fetch_field.flags == Mysql::Field::NUM_FLAG|Mysql::Field::PRI_KEY_FLAG|Mysql::Field::PART_KEY_FLAG|Mysql::Field::NOT_NULL_FLAG } 788 | assert{ @res.fetch_field.flags == 0 } 789 | end 790 | 791 | test '#decimals is number of decimal digits' do 792 | assert{ @m.query('select 1.23').fetch_field.decimals == 2 } 793 | end 794 | 795 | test '#to_hash return field as hash' do 796 | assert{ @res.fetch_field.to_hash == { 797 | 'name' => 'id', 798 | 'table' => 't', 799 | 'def' => nil, 800 | 'type' => Mysql::Field::TYPE_LONG, 801 | 'length' => 11, 802 | 'max_length' => 1, 803 | 'flags' => Mysql::Field::NUM_FLAG|Mysql::Field::PRI_KEY_FLAG|Mysql::Field::PART_KEY_FLAG|Mysql::Field::NOT_NULL_FLAG, 804 | 'decimals' => 0, 805 | } 806 | } 807 | assert{ @res.fetch_field.to_hash == { 808 | 'name' => 'str', 809 | 'table' => 't', 810 | 'def' => nil, 811 | 'type' => Mysql::Field::TYPE_STRING, 812 | 'length' => 10, 813 | 'max_length' => 4, 814 | 'flags' => 0, 815 | 'decimals' => 0, 816 | } 817 | } 818 | end 819 | 820 | test '#inspect returns "#"' do 821 | assert{ @res.fetch_field.inspect == '#' } 822 | assert{ @res.fetch_field.inspect == '#' } 823 | end 824 | 825 | test '#is_num? returns true if the field is numeric' do 826 | assert{ @res.fetch_field.is_num? == true } 827 | assert{ @res.fetch_field.is_num? == false } 828 | end 829 | 830 | test '#is_not_null? returns true if the field is not null' do 831 | assert{ @res.fetch_field.is_not_null? == true } 832 | assert{ @res.fetch_field.is_not_null? == false } 833 | end 834 | 835 | test '#is_pri_key? returns true if the field is primary key' do 836 | assert{ @res.fetch_field.is_pri_key? == true } 837 | assert{ @res.fetch_field.is_pri_key? == false } 838 | end 839 | end 840 | 841 | sub_test_case 'create Mysql::Stmt object:' do 842 | setup do 843 | @m = Mysql.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 844 | end 845 | 846 | teardown do 847 | @m.close if @m 848 | end 849 | 850 | test 'Mysql#stmt returns Mysql::Stmt object' do 851 | assert{ @m.stmt.kind_of? Mysql::Stmt } 852 | end 853 | 854 | test 'Mysq;#prepare returns Mysql::Stmt object' do 855 | assert{ @m.prepare("select 1").kind_of? Mysql::Stmt } 856 | end 857 | end 858 | 859 | sub_test_case 'Mysql::Stmt' do 860 | setup do 861 | @m = Mysql.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 862 | @m.query("set sql_mode=''") 863 | @s = @m.stmt 864 | end 865 | 866 | teardown do 867 | @s.close if @s rescue nil 868 | @m.close if @m rescue nil 869 | end 870 | 871 | test '#affected_rows returns number of affected records' do 872 | @m.query 'create temporary table t (i int, c char(10))' 873 | @s.prepare 'insert into t values (?,?)' 874 | @s.execute 1, 'hoge' 875 | assert{ @s.affected_rows == 1 } 876 | @s.execute 2, 'hoge' 877 | @s.execute 3, 'hoge' 878 | @s.prepare 'update t set c=?' 879 | @s.execute 'fuga' 880 | assert{ @s.affected_rows == 3 } 881 | end 882 | 883 | test '#close returns nil' do 884 | assert{ @s.close == nil } 885 | end 886 | 887 | test '#data_seek set position of current record' do 888 | @m.query 'create temporary table t (i int)' 889 | @m.query 'insert into t values (0),(1),(2),(3),(4),(5),(6)' 890 | @s.prepare 'select i from t' 891 | @s.execute 892 | assert{ @s.fetch == [0] } 893 | assert{ @s.fetch == [1] } 894 | assert{ @s.fetch == [2] } 895 | @s.data_seek 5 896 | assert{ @s.fetch == [5] } 897 | @s.data_seek 1 898 | assert{ @s.fetch == [1] } 899 | end 900 | 901 | test '#each iterate block with a record' do 902 | @m.query 'create temporary table t (i int, c char(255), d datetime)' 903 | @m.query "insert into t values (1,'abc','19701224235905'),(2,'def','21120903123456'),(3,'123',null)" 904 | @s.prepare 'select * from t' 905 | @s.execute 906 | expect = [ 907 | [1, 'abc', Time.new(1970,12,24,23,59,05)], 908 | [2, 'def', Time.new(2112,9,3,12,34,56)], 909 | [3, '123', nil], 910 | ] 911 | @s.each do |a| 912 | assert{ a == expect.shift } 913 | end 914 | end 915 | 916 | test '#execute returns self' do 917 | @s.prepare 'select 1' 918 | assert{ @s.execute == @s } 919 | end 920 | 921 | test '#execute pass arguments to query' do 922 | @m.query 'create temporary table t (i int)' 923 | @s.prepare 'insert into t values (?)' 924 | @s.execute 123 925 | @s.execute '456' 926 | assert{ @m.query('select * from t').entries == [['123'], ['456']] } 927 | end 928 | 929 | test '#execute with various arguments' do 930 | @m.query 'create temporary table t (i int, c char(255), t timestamp)' 931 | @s.prepare 'insert into t values (?,?,?)' 932 | @s.execute 123, 'hoge', Time.local(2009,12,8,19,56,21) 933 | assert{ @m.query('select * from t').fetch_row == ['123', 'hoge', '2009-12-08 19:56:21'] } 934 | end 935 | 936 | test '#execute with arguments that is invalid count raise error' do 937 | @s.prepare 'select ?' 938 | assert_raise Mysql::ClientError, 'parameter count mismatch' do 939 | @s.execute 123, 456 940 | end 941 | end 942 | 943 | test '#execute with huge value' do 944 | [30, 31, 32, 62, 63].each do |i| 945 | assert{ @m.prepare('select cast(? as signed)').execute(2**i-1).fetch == [2**i-1] } 946 | assert{ @m.prepare('select cast(? as signed)').execute(-(2**i)).fetch == [-2**i] } 947 | end 948 | end 949 | 950 | sub_test_case '#execute with various integer value:' do 951 | setup do 952 | @m.query('create temporary table t (i bigint)') 953 | end 954 | [ 955 | -9223372036854775808, 956 | -9223372036854775807, 957 | -4294967297, 958 | -4294967296, 959 | -4294967295, 960 | -2147483649, 961 | -2147483648, 962 | -2147483647, 963 | -65537, 964 | -65536, 965 | -65535, 966 | -32769, 967 | -32768, 968 | -32767, 969 | -257, 970 | -256, 971 | -255, 972 | -129, 973 | -128, 974 | -127, 975 | 0, 976 | 126, 977 | 127, 978 | 128, 979 | 254, 980 | 255, 981 | 256, 982 | 32766, 983 | 32767, 984 | 32768, 985 | 65534, 986 | 65535, 987 | 65536, 988 | 2147483646, 989 | 2147483647, 990 | 2147483648, 991 | 4294967294, 992 | 4294967295, 993 | 4294967296, 994 | 9223372036854775806, 995 | 9223372036854775807, 996 | ].each do |n| 997 | test "#{n} is #{n}" do 998 | @s.prepare 'insert into t values (?)' 999 | @s.execute n 1000 | assert{ @m.query('select i from t').fetch == ["#{n}"] } 1001 | end 1002 | end 1003 | end 1004 | 1005 | sub_test_case '#execute with various unsigned integer value:' do 1006 | setup do 1007 | @m.query('create temporary table t (i bigint unsigned)') 1008 | end 1009 | [ 1010 | 0, 1011 | 126, 1012 | 127, 1013 | 128, 1014 | 254, 1015 | 255, 1016 | 256, 1017 | 32766, 1018 | 32767, 1019 | 32768, 1020 | 65534, 1021 | 65535, 1022 | 65536, 1023 | 2147483646, 1024 | 2147483647, 1025 | 2147483648, 1026 | 4294967294, 1027 | 4294967295, 1028 | 4294967296, 1029 | 9223372036854775806, 1030 | 9223372036854775807, 1031 | 9223372036854775808, 1032 | 18446744073709551614, 1033 | 18446744073709551615, 1034 | ].each do |n| 1035 | test "#{n} is #{n}" do 1036 | @s.prepare 'insert into t values (?)' 1037 | @s.execute n 1038 | assert{ @m.query('select i from t').fetch == ["#{n}"] } 1039 | end 1040 | end 1041 | end 1042 | 1043 | test '#fetch returns result-record' do 1044 | @s.prepare 'select 123, "abc", null' 1045 | @s.execute 1046 | assert{ @s.fetch == [123, 'abc', nil] } 1047 | end 1048 | 1049 | test '#fetch bit column (8bit)' do 1050 | @m.query 'create temporary table t (i bit(8))' 1051 | @m.query 'insert into t values (0),(-1),(127),(-128),(255),(-255),(256)' 1052 | @s.prepare 'select i from t' 1053 | @s.execute 1054 | assert{ @s.entries == [ 1055 | ["\x00".force_encoding('ASCII-8BIT')], 1056 | ["\xff".force_encoding('ASCII-8BIT')], 1057 | ["\x7f".force_encoding('ASCII-8BIT')], 1058 | ["\xff".force_encoding('ASCII-8BIT')], 1059 | ["\xff".force_encoding('ASCII-8BIT')], 1060 | ["\xff".force_encoding('ASCII-8BIT')], 1061 | ["\xff".force_encoding('ASCII-8BIT')], 1062 | ] 1063 | } 1064 | end 1065 | 1066 | test '#fetch bit column (64bit)' do 1067 | @m.query 'create temporary table t (i bit(64))' 1068 | @m.query 'insert into t values (0),(-1),(4294967296),(18446744073709551615),(18446744073709551616)' 1069 | @s.prepare 'select i from t' 1070 | @s.execute 1071 | assert{ @s.entries == [ 1072 | ["\x00\x00\x00\x00\x00\x00\x00\x00".force_encoding('ASCII-8BIT')], 1073 | ["\xff\xff\xff\xff\xff\xff\xff\xff".force_encoding('ASCII-8BIT')], 1074 | ["\x00\x00\x00\x01\x00\x00\x00\x00".force_encoding('ASCII-8BIT')], 1075 | ["\xff\xff\xff\xff\xff\xff\xff\xff".force_encoding('ASCII-8BIT')], 1076 | ["\xff\xff\xff\xff\xff\xff\xff\xff".force_encoding('ASCII-8BIT')], 1077 | ] 1078 | } 1079 | end 1080 | 1081 | test '#fetch tinyint column' do 1082 | @m.query 'create temporary table t (i tinyint)' 1083 | @m.query 'insert into t values (0),(-1),(127),(-128),(255),(-255)' 1084 | @s.prepare 'select i from t' 1085 | @s.execute 1086 | assert{ @s.entries == [[0], [-1], [127], [-128], [127], [-128]] } 1087 | end 1088 | 1089 | test '#fetch tinyint unsigned column' do 1090 | @m.query 'create temporary table t (i tinyint unsigned)' 1091 | @m.query 'insert into t values (0),(-1),(127),(-128),(255),(-255),(256)' 1092 | @s.prepare 'select i from t' 1093 | @s.execute 1094 | assert{ @s.entries == [[0], [0], [127], [0], [255], [0], [255]] } 1095 | end 1096 | 1097 | test '#fetch smallint column' do 1098 | @m.query 'create temporary table t (i smallint)' 1099 | @m.query 'insert into t values (0),(-1),(32767),(-32768),(65535),(-65535),(65536)' 1100 | @s.prepare 'select i from t' 1101 | @s.execute 1102 | assert{ @s.entries == [[0], [-1], [32767], [-32768], [32767], [-32768], [32767]] } 1103 | end 1104 | 1105 | test '#fetch smallint unsigned column' do 1106 | @m.query 'create temporary table t (i smallint unsigned)' 1107 | @m.query 'insert into t values (0),(-1),(32767),(-32768),(65535),(-65535),(65536)' 1108 | @s.prepare 'select i from t' 1109 | @s.execute 1110 | assert{ @s.entries == [[0], [0], [32767], [0], [65535], [0], [65535]] } 1111 | end 1112 | 1113 | test '#fetch mediumint column' do 1114 | @m.query 'create temporary table t (i mediumint)' 1115 | @m.query 'insert into t values (0),(-1),(8388607),(-8388608),(16777215),(-16777215),(16777216)' 1116 | @s.prepare 'select i from t' 1117 | @s.execute 1118 | assert{ @s.entries == [[0], [-1], [8388607], [-8388608], [8388607], [-8388608], [8388607]] } 1119 | end 1120 | 1121 | test '#fetch mediumint unsigned column' do 1122 | @m.query 'create temporary table t (i mediumint unsigned)' 1123 | @m.query 'insert into t values (0),(-1),(8388607),(-8388608),(16777215),(-16777215),(16777216)' 1124 | @s.prepare 'select i from t' 1125 | @s.execute 1126 | assert{ @s.entries == [[0], [0], [8388607], [0], [16777215], [0], [16777215]] } 1127 | end 1128 | 1129 | test '#fetch int column' do 1130 | @m.query 'create temporary table t (i int)' 1131 | @m.query 'insert into t values (0),(-1),(2147483647),(-2147483648),(4294967295),(-4294967295),(4294967296)' 1132 | @s.prepare 'select i from t' 1133 | @s.execute 1134 | assert{ @s.entries == [[0], [-1], [2147483647], [-2147483648], [2147483647], [-2147483648], [2147483647]] } 1135 | end 1136 | 1137 | test '#fetch int unsigned column' do 1138 | @m.query 'create temporary table t (i int unsigned)' 1139 | @m.query 'insert into t values (0),(-1),(2147483647),(-2147483648),(4294967295),(-4294967295),(4294967296)' 1140 | @s.prepare 'select i from t' 1141 | @s.execute 1142 | assert{ @s.entries == [[0], [0], [2147483647], [0], [4294967295], [0], [4294967295]] } 1143 | end 1144 | 1145 | test '#fetch bigint column' do 1146 | @m.query 'create temporary table t (i bigint)' 1147 | @m.query 'insert into t values (0),(-1),(9223372036854775807),(-9223372036854775808),(18446744073709551615),(-18446744073709551615),(18446744073709551616)' 1148 | @s.prepare 'select i from t' 1149 | @s.execute 1150 | assert{ @s.entries == [[0], [-1], [9223372036854775807], [-9223372036854775808], [9223372036854775807], [-9223372036854775808], [9223372036854775807]] } 1151 | end 1152 | 1153 | test '#fetch bigint unsigned column' do 1154 | @m.query 'create temporary table t (i bigint unsigned)' 1155 | @m.query 'insert into t values (0),(-1),(9223372036854775807),(-9223372036854775808),(18446744073709551615),(-18446744073709551615),(18446744073709551616)' 1156 | @s.prepare 'select i from t' 1157 | @s.execute 1158 | assert{ @s.entries == [[0], [0], [9223372036854775807], [0], [18446744073709551615], [0], [18446744073709551615]] } 1159 | end 1160 | 1161 | test '#fetch float column' do 1162 | @m.query 'create temporary table t (i float)' 1163 | @m.query 'insert into t values (0),(-3.402823466E+38),(-1.175494351E-38),(1.175494351E-38),(3.402823466E+38)' 1164 | @s.prepare 'select i from t' 1165 | @s.execute 1166 | assert{ @s.fetch[0] == 0.0 } 1167 | assert{ (@s.fetch[0] - -3.402823466E+38).abs < 0.000000001E+38 } 1168 | assert{ (@s.fetch[0] - -1.175494351E-38).abs < 0.000000001E-38 } 1169 | assert{ (@s.fetch[0] - 1.175494351E-38).abs < 0.000000001E-38 } 1170 | assert{ (@s.fetch[0] - 3.402823466E+38).abs < 0.000000001E+38 } 1171 | end 1172 | 1173 | test '#fetch float unsigned column' do 1174 | @m.query 'create temporary table t (i float unsigned)' 1175 | @m.query 'insert into t values (0),(-3.402823466E+38),(-1.175494351E-38),(1.175494351E-38),(3.402823466E+38)' 1176 | @s.prepare 'select i from t' 1177 | @s.execute 1178 | assert{ @s.fetch[0] == 0.0 } 1179 | assert{ @s.fetch[0] == 0.0 } 1180 | assert{ @s.fetch[0] == 0.0 } 1181 | assert{ (@s.fetch[0] - 1.175494351E-38).abs < 0.000000001E-38 } 1182 | assert{ (@s.fetch[0] - 3.402823466E+38).abs < 0.000000001E+38 } 1183 | end 1184 | 1185 | test '#fetch double column' do 1186 | @m.query 'create temporary table t (i double)' 1187 | @m.query 'insert into t values (0),(-1.7976931348623157E+308),(-2.2250738585072014E-308),(2.2250738585072014E-308),(1.7976931348623157E+308)' 1188 | @s.prepare 'select i from t' 1189 | @s.execute 1190 | assert{ @s.fetch[0] == 0.0 } 1191 | assert{ (@s.fetch[0] - -Float::MAX).abs < Float::EPSILON } 1192 | assert{ (@s.fetch[0] - -Float::MIN).abs < Float::EPSILON } 1193 | assert{ (@s.fetch[0] - Float::MIN).abs < Float::EPSILON } 1194 | assert{ (@s.fetch[0] - Float::MAX).abs < Float::EPSILON } 1195 | end 1196 | 1197 | test '#fetch double unsigned column' do 1198 | @m.query 'create temporary table t (i double unsigned)' 1199 | @m.query 'insert into t values (0),(-1.7976931348623157E+308),(-2.2250738585072014E-308),(2.2250738585072014E-308),(1.7976931348623157E+308)' 1200 | @s.prepare 'select i from t' 1201 | @s.execute 1202 | assert{ @s.fetch[0] == 0.0 } 1203 | assert{ @s.fetch[0] == 0.0 } 1204 | assert{ @s.fetch[0] == 0.0 } 1205 | assert{ (@s.fetch[0] - Float::MIN).abs < Float::EPSILON } 1206 | assert{ (@s.fetch[0] - Float::MAX).abs < Float::EPSILON } 1207 | end 1208 | 1209 | test '#fetch decimal column' do 1210 | @m.query 'create temporary table t (i decimal)' 1211 | @m.query 'insert into t values (0),(9999999999),(-9999999999),(10000000000),(-10000000000)' 1212 | @s.prepare 'select i from t' 1213 | @s.execute 1214 | assert{ @s.entries == [["0"], ["9999999999"], ["-9999999999"], ["9999999999"], ["-9999999999"]] } 1215 | end 1216 | 1217 | test '#fetch decimal unsigned column' do 1218 | @m.query 'create temporary table t (i decimal unsigned)' 1219 | @m.query 'insert into t values (0),(9999999998),(9999999999),(-9999999998),(-9999999999),(10000000000),(-10000000000)' 1220 | @s.prepare 'select i from t' 1221 | @s.execute 1222 | assert{ @s.entries == [["0"], ["9999999998"], ["9999999999"], ["0"], ["0"], ["9999999999"], ["0"]] } 1223 | end 1224 | 1225 | test '#fetch date column' do 1226 | @m.query 'create temporary table t (i date)' 1227 | @m.query "insert into t values ('0000-00-00'),('1000-01-01'),('9999-12-31')" 1228 | @s.prepare 'select i from t' 1229 | @s.execute 1230 | cols = @s.fetch 1231 | assert{ cols == [nil] } 1232 | cols = @s.fetch 1233 | assert{ cols == [Time.new(1000,1,1)] } 1234 | cols = @s.fetch 1235 | assert{ cols == [Time.new(9999,12,31)] } 1236 | end 1237 | 1238 | test '#fetch datetime column' do 1239 | @m.query 'create temporary table t (i datetime)' 1240 | @m.query "insert into t values ('0000-00-00 00:00:00'),('1000-01-01 00:00:00'),('9999-12-31 23:59:59')" 1241 | @s.prepare 'select i from t' 1242 | @s.execute 1243 | assert{ @s.fetch == [nil] } 1244 | assert{ @s.fetch == [Time.new(1000,1,1)] } 1245 | assert{ @s.fetch == [Time.new(9999,12,31,23,59,59)] } 1246 | end 1247 | 1248 | test '#fetch timestamp column' do 1249 | @m.query 'create temporary table t (i timestamp)' 1250 | @m.query("insert into t values ('1970-01-02 00:00:00'),('2037-12-30 23:59:59')") 1251 | @s.prepare 'select i from t' 1252 | @s.execute 1253 | assert{ @s.fetch == [Time.new(1970,1,2)] } 1254 | assert{ @s.fetch == [Time.new(2037,12,30,23,59,59)] } 1255 | end 1256 | 1257 | test '#fetch time column' do 1258 | @m.query 'create temporary table t (i time)' 1259 | @m.query "insert into t values ('-838:59:59'),(0),('838:59:59')" 1260 | @s.prepare 'select i from t' 1261 | @s.execute 1262 | assert{ @s.fetch == [-(838*3600+59*60+59)] } 1263 | assert{ @s.fetch == [0] } 1264 | assert{ @s.fetch == [838*3600+59*60+59] } 1265 | end 1266 | 1267 | test '#fetch year column' do 1268 | @m.query 'create temporary table t (i year)' 1269 | @m.query 'insert into t values (0),(70),(69),(1901),(2155)' 1270 | @s.prepare 'select i from t' 1271 | @s.execute 1272 | assert{ @s.entries == [[0], [1970], [2069], [1901], [2155]] } 1273 | end 1274 | 1275 | test '#fetch char column' do 1276 | @m.query 'create temporary table t (i char(10))' 1277 | @m.query "insert into t values (null),('abc')" 1278 | @s.prepare 'select i from t' 1279 | @s.execute 1280 | assert{ @s.entries == [[nil], ['abc']] } 1281 | end 1282 | 1283 | test '#fetch varchar column' do 1284 | @m.query 'create temporary table t (i varchar(10))' 1285 | @m.query "insert into t values (null),('abc')" 1286 | @s.prepare 'select i from t' 1287 | @s.execute 1288 | assert{ @s.entries == [[nil], ['abc']] } 1289 | end 1290 | 1291 | test '#fetch binary column' do 1292 | @m.query 'create temporary table t (i binary(10))' 1293 | @m.query "insert into t values (null),('abc')" 1294 | @s.prepare 'select i from t' 1295 | @s.execute 1296 | assert{ @s.entries == [[nil], ["abc\0\0\0\0\0\0\0"]] } 1297 | end 1298 | 1299 | test '#fetch varbinary column' do 1300 | @m.query 'create temporary table t (i varbinary(10))' 1301 | @m.query "insert into t values (null),('abc')" 1302 | @s.prepare 'select i from t' 1303 | @s.execute 1304 | assert{ @s.entries == [[nil], ["abc"]] } 1305 | end 1306 | 1307 | test '#fetch tinyblob column' do 1308 | @m.query 'create temporary table t (i tinyblob)' 1309 | @m.query "insert into t values (null),('#{"a"*255}')" 1310 | @s.prepare 'select i from t' 1311 | @s.execute 1312 | assert{ @s.entries == [[nil], ["a"*255]] } 1313 | end 1314 | 1315 | test '#fetch tinytext column' do 1316 | @m.query 'create temporary table t (i tinytext)' 1317 | @m.query "insert into t values (null),('#{"a"*255}')" 1318 | @s.prepare 'select i from t' 1319 | @s.execute 1320 | assert{ @s.entries == [[nil], ["a"*255]] } 1321 | end 1322 | 1323 | test '#fetch blob column' do 1324 | @m.query 'create temporary table t (i blob)' 1325 | @m.query "insert into t values (null),('#{"a"*65535}')" 1326 | @s.prepare 'select i from t' 1327 | @s.execute 1328 | assert{ @s.entries == [[nil], ["a"*65535]] } 1329 | end 1330 | 1331 | test '#fetch text column' do 1332 | @m.query 'create temporary table t (i text)' 1333 | @m.query "insert into t values (null),('#{"a"*65535}')" 1334 | @s.prepare 'select i from t' 1335 | @s.execute 1336 | assert{ @s.entries == [[nil], ["a"*65535]] } 1337 | end 1338 | 1339 | test '#fetch mediumblob column' do 1340 | @m.query 'create temporary table t (i mediumblob)' 1341 | @m.query "insert into t values (null),('#{"a"*16777215}')" 1342 | @s.prepare 'select i from t' 1343 | @s.execute 1344 | assert{ @s.entries == [[nil], ['a'*16777215]] } 1345 | end 1346 | 1347 | test '#fetch mediumtext column' do 1348 | @m.query 'create temporary table t (i mediumtext)' 1349 | @m.query "insert into t values (null),('#{"a"*16777215}')" 1350 | @s.prepare 'select i from t' 1351 | @s.execute 1352 | assert{ @s.entries == [[nil], ['a'*16777215]] } 1353 | end 1354 | 1355 | test '#fetch longblob column' do 1356 | @m.query 'create temporary table t (i longblob)' 1357 | @m.query "insert into t values (null),('#{"a"*16777216}')" 1358 | @s.prepare 'select i from t' 1359 | @s.execute 1360 | assert{ @s.entries == [[nil], ["a"*16777216]] } 1361 | end 1362 | 1363 | test '#fetch longtext column' do 1364 | @m.query 'create temporary table t (i longtext)' 1365 | @m.query "insert into t values (null),('#{"a"*16777216}')" 1366 | @s.prepare 'select i from t' 1367 | @s.execute 1368 | assert{ @s.entries == [[nil], ["a"*16777216]] } 1369 | end 1370 | 1371 | test '#fetch enum column' do 1372 | @m.query "create temporary table t (i enum('abc','def'))" 1373 | @m.query "insert into t values (null),(0),(1),(2),('abc'),('def'),('ghi')" 1374 | @s.prepare 'select i from t' 1375 | @s.execute 1376 | assert{ @s.entries == [[nil], [''], ['abc'], ['def'], ['abc'], ['def'], ['']] } 1377 | end 1378 | 1379 | test '#fetch set column' do 1380 | @m.query "create temporary table t (i set('abc','def'))" 1381 | @m.query "insert into t values (null),(0),(1),(2),(3),('abc'),('def'),('abc,def'),('ghi')" 1382 | @s.prepare 'select i from t' 1383 | @s.execute 1384 | assert{ @s.entries == [[nil], [''], ['abc'], ['def'], ['abc,def'], ['abc'], ['def'], ['abc,def'], ['']] } 1385 | end 1386 | 1387 | test '#fetch json column' do 1388 | if @m.server_version >= 50700 1389 | @m.query "create temporary table t (i json)" 1390 | @m.query "insert into t values ('123'),('{\"a\":1,\"b\":2,\"c\":3}'),('[1,2,3]')" 1391 | @s.prepare 'select i from t' 1392 | @s.execute 1393 | assert{ @s.entries == [['123'], ['{"a": 1, "b": 2, "c": 3}'], ['[1, 2, 3]']] } 1394 | end 1395 | end 1396 | 1397 | test '#field_count' do 1398 | @s.prepare 'select 1,2,3' 1399 | assert{ @s.field_count == 3 } 1400 | @s.prepare 'set @a=1' 1401 | assert{ @s.field_count == 0 } 1402 | end 1403 | 1404 | test '#free_result' do 1405 | @s.free_result 1406 | @s.prepare 'select 1,2,3' 1407 | @s.execute 1408 | @s.free_result 1409 | end 1410 | 1411 | test '#insert_id' do 1412 | @m.query 'create temporary table t (i int auto_increment, unique(i))' 1413 | @s.prepare 'insert into t values (0)' 1414 | @s.execute 1415 | assert{ @s.insert_id == 1 } 1416 | @s.execute 1417 | assert{ @s.insert_id == 2 } 1418 | end 1419 | 1420 | test '#num_rows' do 1421 | @m.query 'create temporary table t (i int)' 1422 | @m.query 'insert into t values (1),(2),(3),(4)' 1423 | @s.prepare 'select * from t' 1424 | @s.execute 1425 | assert{ @s.num_rows == 4 } 1426 | end 1427 | 1428 | test '#param_count' do 1429 | @m.query 'create temporary table t (a int, b int, c int)' 1430 | @s.prepare 'select * from t' 1431 | assert{ @s.param_count == 0 } 1432 | @s.prepare 'insert into t values (?,?,?)' 1433 | assert{ @s.param_count == 3 } 1434 | end 1435 | 1436 | test '#prepare' do 1437 | assert{ @s.prepare('select 1').kind_of? Mysql::Stmt } 1438 | assert_raise Mysql::ParseError do 1439 | @s.prepare 'invalid syntax' 1440 | end 1441 | end 1442 | 1443 | test '#prepare returns self' do 1444 | assert{ @s.prepare('select 1') == @s } 1445 | end 1446 | 1447 | test '#prepare with invalid query raises error' do 1448 | assert_raise Mysql::ParseError do 1449 | @s.prepare 'invalid query' 1450 | end 1451 | end 1452 | 1453 | test '#result_metadata' do 1454 | @s.prepare 'select 1 foo, 2 bar' 1455 | f = @s.result_metadata.fetch_fields 1456 | assert{ f[0].name == 'foo' } 1457 | assert{ f[1].name == 'bar' } 1458 | end 1459 | 1460 | test '#result_metadata forn no data' do 1461 | @s.prepare 'set @a=1' 1462 | assert{ @s.result_metadata == nil } 1463 | end 1464 | 1465 | test '#row_seek and #row_tell' do 1466 | @m.query 'create temporary table t (i int)' 1467 | @m.query 'insert into t values (0),(1),(2),(3),(4)' 1468 | @s.prepare 'select * from t' 1469 | @s.execute 1470 | row0 = @s.row_tell 1471 | assert{ @s.fetch == [0] } 1472 | assert{ @s.fetch == [1] } 1473 | row2 = @s.row_seek row0 1474 | assert{ @s.fetch == [0] } 1475 | @s.row_seek row2 1476 | assert{ @s.fetch == [2] } 1477 | end 1478 | 1479 | test '#sqlstate' do 1480 | @s.prepare 'select 1' 1481 | assert{ @s.sqlstate == '00000' } 1482 | assert_raise Mysql::ParseError do 1483 | @s.prepare 'hogehoge' 1484 | end 1485 | assert{ @s.sqlstate == '42000' } 1486 | end 1487 | end 1488 | 1489 | sub_test_case 'Mysql::Error' do 1490 | setup do 1491 | m = Mysql.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 1492 | begin 1493 | m.query('hogehoge') 1494 | rescue => @e 1495 | end 1496 | end 1497 | 1498 | test '#error is error message' do 1499 | assert{ @e.error == "You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'hogehoge' at line 1" } 1500 | end 1501 | 1502 | test '#errno is error number' do 1503 | assert{ @e.errno == 1064 } 1504 | end 1505 | 1506 | test '#sqlstate is sqlstate value as String' do 1507 | assert{ @e.sqlstate == '42000' } 1508 | end 1509 | end 1510 | 1511 | sub_test_case 'Connection charset is UTF-8:' do 1512 | setup do 1513 | @m = Mysql.connect(MYSQL_SERVER, MYSQL_USER, MYSQL_PASSWORD, MYSQL_DATABASE, MYSQL_PORT, MYSQL_SOCKET) 1514 | @m.charset = "utf8" 1515 | @m.query "create temporary table t (utf8 char(10) charset utf8, cp932 char(10) charset cp932, eucjp char(10) charset eucjpms, bin varbinary(10))" 1516 | @utf8 = "いろは" 1517 | @cp932 = @utf8.encode "CP932" 1518 | @eucjp = @utf8.encode "EUC-JP-MS" 1519 | @bin = "\x00\x01\x7F\x80\xFE\xFF".force_encoding("ASCII-8BIT") 1520 | @default_internal = Encoding.default_internal 1521 | end 1522 | 1523 | teardown do 1524 | v = $VERBOSE 1525 | $VERBOSE = false 1526 | Encoding.default_internal = @default_internal 1527 | $VERBOSE = v 1528 | end 1529 | 1530 | sub_test_case 'default_internal is CP932' do 1531 | setup do 1532 | @m.prepare("insert into t (utf8,cp932,eucjp,bin) values (?,?,?,?)").execute @utf8, @cp932, @eucjp, @bin 1533 | v = $VERBOSE 1534 | $VERBOSE = false 1535 | Encoding.default_internal = 'CP932' 1536 | $VERBOSE = v 1537 | end 1538 | test 'is converted to CP932' do 1539 | assert @m.query('select "あいう"').fetch == ["\x82\xA0\x82\xA2\x82\xA4".force_encoding("CP932")] 1540 | end 1541 | test 'data is stored as is' do 1542 | assert @m.query('select hex(utf8),hex(cp932),hex(eucjp),hex(bin) from t').fetch == ['E38184E3828DE381AF', '82A282EB82CD', 'A4A4A4EDA4CF', '00017F80FEFF'] 1543 | end 1544 | test 'By simple query, charset of retrieved data is connection charset' do 1545 | assert @m.query('select utf8,cp932,eucjp,bin from t').fetch == [@cp932, @cp932, @cp932, @bin] 1546 | end 1547 | test 'By prepared statement, charset of retrieved data is connection charset except for binary' do 1548 | assert @m.prepare('select utf8,cp932,eucjp,bin from t').execute.fetch == [@cp932, @cp932, @cp932, @bin] 1549 | end 1550 | end 1551 | 1552 | sub_test_case 'query with CP932 encoding' do 1553 | test 'is converted to UTF-8' do 1554 | assert @m.query('select HEX("あいう")'.encode("CP932")).fetch == ["E38182E38184E38186"] 1555 | end 1556 | end 1557 | 1558 | sub_test_case 'prepared statement with CP932 encoding' do 1559 | test 'is converted to UTF-8' do 1560 | assert @m.prepare('select HEX("あいう")'.encode("CP932")).execute.fetch == ["E38182E38184E38186"] 1561 | end 1562 | end 1563 | 1564 | sub_test_case 'The encoding of data are correspond to charset of column:' do 1565 | setup do 1566 | @m.prepare("insert into t (utf8,cp932,eucjp,bin) values (?,?,?,?)").execute @utf8, @cp932, @eucjp, @bin 1567 | end 1568 | test 'data is stored as is' do 1569 | assert{ @m.query('select hex(utf8),hex(cp932),hex(eucjp),hex(bin) from t').fetch == ['E38184E3828DE381AF', '82A282EB82CD', 'A4A4A4EDA4CF', '00017F80FEFF'] } 1570 | end 1571 | test 'By simple query, charset of retrieved data is connection charset' do 1572 | assert{ @m.query('select utf8,cp932,eucjp,bin from t').fetch == [@utf8, @utf8, @utf8, @bin] } 1573 | end 1574 | test 'By prepared statement, charset of retrieved data is connection charset except for binary' do 1575 | assert{ @m.prepare('select utf8,cp932,eucjp,bin from t').execute.fetch == [@utf8, @utf8, @utf8, @bin] } 1576 | end 1577 | end 1578 | 1579 | sub_test_case 'The encoding of data are different from charset of column:' do 1580 | setup do 1581 | @m.prepare("insert into t (utf8,cp932,eucjp,bin) values (?,?,?,?)").execute @utf8, @utf8, @utf8, @utf8 1582 | end 1583 | test 'stored data is converted' do 1584 | assert{ @m.query("select hex(utf8),hex(cp932),hex(eucjp),hex(bin) from t").fetch == ["E38184E3828DE381AF", "82A282EB82CD", "A4A4A4EDA4CF", "E38184E3828DE381AF"] } 1585 | end 1586 | test 'By simple query, charset of retrieved data is connection charset' do 1587 | assert{ @m.query("select utf8,cp932,eucjp,bin from t").fetch == [@utf8, @utf8, @utf8, @utf8.dup.force_encoding('ASCII-8BIT')] } 1588 | end 1589 | test 'By prepared statement, charset of retrieved data is connection charset except for binary' do 1590 | assert{ @m.prepare("select utf8,cp932,eucjp,bin from t").execute.fetch == [@utf8, @utf8, @utf8, @utf8.dup.force_encoding("ASCII-8BIT")] } 1591 | end 1592 | end 1593 | 1594 | sub_test_case 'The data include invalid byte code:' do 1595 | test 'raises Encoding::InvalidByteSequenceError' do 1596 | cp932 = "\x01\xFF\x80".force_encoding("CP932") 1597 | assert_raise Encoding::InvalidByteSequenceError do 1598 | @m.prepare("insert into t (cp932) values (?)").execute cp932 1599 | end 1600 | end 1601 | end 1602 | end 1603 | end 1604 | --------------------------------------------------------------------------------