tmtm-ruby-mysql 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/ChangeLog +3 -0
- data/README +38 -0
- data/Rakefile +144 -0
- data/lib/mysql.rb +708 -0
- data/lib/mysql/cache.rb +26 -0
- data/lib/mysql/charset.rb +255 -0
- data/lib/mysql/compat.rb +209 -0
- data/lib/mysql/constants.rb +164 -0
- data/lib/mysql/error.rb +518 -0
- data/lib/mysql/protocol.rb +558 -0
- metadata +76 -0
data/ChangeLog
ADDED
data/README
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
= ruby-mysql
|
2
|
+
|
3
|
+
== Description
|
4
|
+
MySQL connector for Ruby.
|
5
|
+
|
6
|
+
ALPHA バージョンです。将来のバージョンで互換がない変更がされる可能性あります。
|
7
|
+
|
8
|
+
== Installation
|
9
|
+
|
10
|
+
ruby setup.rb
|
11
|
+
|
12
|
+
=== Gem Installation
|
13
|
+
|
14
|
+
gem install tmtm-ruby-mysql --source http://gems.github.com
|
15
|
+
|
16
|
+
== Features/Problems
|
17
|
+
|
18
|
+
* Ruby だけで書かれているのでコンパイル不要です。
|
19
|
+
* Ruby 1.9 の M17N に対応しています。
|
20
|
+
* Ruby/MySQL 0.x, MySQL/Ruby 2.x とは互換がありません。
|
21
|
+
* 英語ドキュメントがありません。
|
22
|
+
|
23
|
+
== Synopsis
|
24
|
+
|
25
|
+
使用例:
|
26
|
+
|
27
|
+
Mysql.connect("mysql://username@password:hostname:3306/dbname") do |my|
|
28
|
+
my.query("select col1, col2 from tblname").each do |col1, col2|
|
29
|
+
p col1, col2
|
30
|
+
end
|
31
|
+
my.query("insert into tblname (col1,col2) values (?,?)", 123, "abc")
|
32
|
+
end
|
33
|
+
|
34
|
+
== Copyright
|
35
|
+
|
36
|
+
Author:: tommy <tommy@tmtm.org>
|
37
|
+
Copyright:: Copyright (c) 2009 tommy
|
38
|
+
License:: Ruby's
|
data/Rakefile
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/clean'
|
4
|
+
require 'rake/testtask'
|
5
|
+
require 'rake/packagetask'
|
6
|
+
require 'rake/gempackagetask'
|
7
|
+
require 'rake/rdoctask'
|
8
|
+
require 'rake/contrib/rubyforgepublisher'
|
9
|
+
require 'rake/contrib/sshpublisher'
|
10
|
+
require 'fileutils'
|
11
|
+
require 'lib/mysql'
|
12
|
+
include FileUtils
|
13
|
+
|
14
|
+
NAME = "ruby-mysql"
|
15
|
+
AUTHOR = "tommy"
|
16
|
+
EMAIL = "tommy@tmtm.org"
|
17
|
+
DESCRIPTION = "MySQL connector for Ruby"
|
18
|
+
RUBYFORGE_PROJECT = "rubymysql"
|
19
|
+
HOMEPATH = "http://github.com/tmtm/ruby-mysql"
|
20
|
+
BIN_FILES = %w( )
|
21
|
+
|
22
|
+
VERS = "3.0.0"
|
23
|
+
REV = File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil
|
24
|
+
CLEAN.include ['**/.*.sw?', '*.gem', '.config']
|
25
|
+
RDOC_OPTS = [
|
26
|
+
'--title', "#{NAME} documentation",
|
27
|
+
"--charset", "utf-8",
|
28
|
+
"--opname", "index.html",
|
29
|
+
"--line-numbers",
|
30
|
+
"--main", "README",
|
31
|
+
"--inline-source",
|
32
|
+
]
|
33
|
+
|
34
|
+
task :default => [:test]
|
35
|
+
task :package => [:clean]
|
36
|
+
|
37
|
+
Rake::TestTask.new("test") do |t|
|
38
|
+
t.libs << "test"
|
39
|
+
t.pattern = "test/**/*_test.rb"
|
40
|
+
t.verbose = true
|
41
|
+
end
|
42
|
+
|
43
|
+
spec = Gem::Specification.new do |s|
|
44
|
+
s.name = NAME
|
45
|
+
s.version = VERS
|
46
|
+
s.platform = Gem::Platform::RUBY
|
47
|
+
s.has_rdoc = true
|
48
|
+
s.extra_rdoc_files = ["README", "ChangeLog"]
|
49
|
+
s.rdoc_options += RDOC_OPTS + ['--exclude', '^(examples|extras)/']
|
50
|
+
s.summary = DESCRIPTION
|
51
|
+
s.description = DESCRIPTION
|
52
|
+
s.author = AUTHOR
|
53
|
+
s.email = EMAIL
|
54
|
+
s.homepage = HOMEPATH
|
55
|
+
s.executables = BIN_FILES
|
56
|
+
s.rubyforge_project = RUBYFORGE_PROJECT
|
57
|
+
s.bindir = "bin"
|
58
|
+
s.require_path = "lib"
|
59
|
+
#s.autorequire = ""
|
60
|
+
s.test_files = Dir["test/*_test.rb"]
|
61
|
+
|
62
|
+
#s.add_dependency('activesupport', '>=1.3.1')
|
63
|
+
s.required_ruby_version = '>= 1.8.7'
|
64
|
+
|
65
|
+
s.files = %w(README ChangeLog Rakefile) +
|
66
|
+
Dir.glob("{bin,doc,test,lib,templates,generator,extras,website,script}/**/*") +
|
67
|
+
Dir.glob("ext/**/*.{h,c,rb}") +
|
68
|
+
Dir.glob("examples/**/*.rb") +
|
69
|
+
Dir.glob("tools/*.rb") +
|
70
|
+
Dir.glob("rails/*.rb")
|
71
|
+
|
72
|
+
s.extensions = FileList["ext/**/extconf.rb"].to_a
|
73
|
+
end
|
74
|
+
|
75
|
+
Rake::GemPackageTask.new(spec) do |p|
|
76
|
+
p.need_tar = true
|
77
|
+
p.gem_spec = spec
|
78
|
+
end
|
79
|
+
|
80
|
+
task :install do
|
81
|
+
name = "#{NAME}-#{VERS}.gem"
|
82
|
+
sh %{rake package}
|
83
|
+
sh %{sudo gem install pkg/#{name}}
|
84
|
+
end
|
85
|
+
|
86
|
+
task :uninstall => [:clean] do
|
87
|
+
sh %{sudo gem uninstall #{NAME}}
|
88
|
+
end
|
89
|
+
|
90
|
+
|
91
|
+
Rake::RDocTask.new do |rdoc|
|
92
|
+
rdoc.rdoc_dir = 'html'
|
93
|
+
rdoc.options += RDOC_OPTS
|
94
|
+
rdoc.template = "resh"
|
95
|
+
#rdoc.template = "#{ENV['template']}.rb" if ENV['template']
|
96
|
+
if ENV['DOC_FILES']
|
97
|
+
rdoc.rdoc_files.include(ENV['DOC_FILES'].split(/,\s*/))
|
98
|
+
else
|
99
|
+
rdoc.rdoc_files.include('README', 'ChangeLog')
|
100
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
101
|
+
rdoc.rdoc_files.include('ext/**/*.c')
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
desc "Publish to RubyForge"
|
106
|
+
task :rubyforge => [:rdoc, :package] do
|
107
|
+
require 'rubyforge'
|
108
|
+
Rake::RubyForgePublisher.new(RUBYFORGE_PROJECT, 'tommy').upload
|
109
|
+
end
|
110
|
+
|
111
|
+
desc 'Package and upload the release to rubyforge.'
|
112
|
+
task :release => [:clean, :package] do |t|
|
113
|
+
v = ENV["VERSION"] or abort "Must supply VERSION=x.y.z"
|
114
|
+
abort "Versions don't match #{v} vs #{VERS}" unless v == VERS
|
115
|
+
pkg = "pkg/#{NAME}-#{VERS}"
|
116
|
+
|
117
|
+
require 'rubyforge'
|
118
|
+
rf = RubyForge.new.configure
|
119
|
+
puts "Logging in"
|
120
|
+
rf.login
|
121
|
+
|
122
|
+
c = rf.userconfig
|
123
|
+
# c["release_notes"] = description if description
|
124
|
+
# c["release_changes"] = changes if changes
|
125
|
+
c["preformatted"] = true
|
126
|
+
|
127
|
+
files = [
|
128
|
+
"#{pkg}.tgz",
|
129
|
+
"#{pkg}.gem"
|
130
|
+
].compact
|
131
|
+
|
132
|
+
puts "Releasing #{NAME} v. #{VERS}"
|
133
|
+
rf.add_release RUBYFORGE_PROJECT, NAME, VERS, *files
|
134
|
+
end
|
135
|
+
|
136
|
+
desc 'Show information about the gem.'
|
137
|
+
task :debug_gem do
|
138
|
+
puts spec.to_ruby
|
139
|
+
end
|
140
|
+
|
141
|
+
desc 'Update gem spec'
|
142
|
+
task :gemspec do
|
143
|
+
open("#{NAME}.gemspec", 'w').write spec.to_ruby
|
144
|
+
end
|
data/lib/mysql.rb
ADDED
@@ -0,0 +1,708 @@
|
|
1
|
+
# Copyright (C) 2008 TOMITA Masahiro
|
2
|
+
# mailto:tommy@tmtm.org
|
3
|
+
|
4
|
+
$LOAD_PATH.unshift File.dirname(__FILE__)
|
5
|
+
require "mysql/constants"
|
6
|
+
require "mysql/error"
|
7
|
+
require "mysql/charset"
|
8
|
+
require "mysql/protocol"
|
9
|
+
require "mysql/cache"
|
10
|
+
|
11
|
+
class Mysql
|
12
|
+
|
13
|
+
VERSION = 30000 # Version number of this library
|
14
|
+
MYSQL_UNIX_PORT = "/tmp/mysql.sock" # UNIX domain socket filename
|
15
|
+
MYSQL_TCP_PORT = 3306 # TCP socket port number
|
16
|
+
|
17
|
+
OPTIONS = {
|
18
|
+
:connect_timeout => Integer,
|
19
|
+
# :compress => x,
|
20
|
+
# :named_pipe => x,
|
21
|
+
:init_command => String,
|
22
|
+
# :read_default_file => x,
|
23
|
+
# :read_default_group => x,
|
24
|
+
:charset => Object,
|
25
|
+
# :local_infile => x,
|
26
|
+
# :shared_memory_base_name => x,
|
27
|
+
:read_timeout => Integer,
|
28
|
+
:write_timeout => Integer,
|
29
|
+
# :use_result => x,
|
30
|
+
# :use_remote_connection => x,
|
31
|
+
# :use_embedded_connection => x,
|
32
|
+
# :guess_connection => x,
|
33
|
+
# :client_ip => x,
|
34
|
+
# :secure_auth => x,
|
35
|
+
# :report_data_truncation => x,
|
36
|
+
# :reconnect => x,
|
37
|
+
# :ssl_verify_server_cert => x,
|
38
|
+
:prepared_statement_cache_size => Integer,
|
39
|
+
} # :nodoc:
|
40
|
+
|
41
|
+
OPT2FLAG = {
|
42
|
+
# :compress => CLIENT_COMPRESS,
|
43
|
+
:found_rows => CLIENT_FOUND_ROWS,
|
44
|
+
:ignore_sigpipe => CLIENT_IGNORE_SIGPIPE,
|
45
|
+
:ignore_space => CLIENT_IGNORE_SPACE,
|
46
|
+
:interactive => CLIENT_INTERACTIVE,
|
47
|
+
:local_files => CLIENT_LOCAL_FILES,
|
48
|
+
# :multi_results => CLIENT_MULTI_RESULTS,
|
49
|
+
# :multi_statements => CLIENT_MULTI_STATEMENTS,
|
50
|
+
:no_schema => CLIENT_NO_SCHEMA,
|
51
|
+
# :ssl => CLIENT_SSL,
|
52
|
+
} # :nodoc:
|
53
|
+
|
54
|
+
attr_reader :charset # character set of MySQL connection
|
55
|
+
attr_reader :affected_rows # number of affected records by insert/update/delete.
|
56
|
+
attr_reader :insert_id # latest auto_increment value.
|
57
|
+
attr_reader :server_status # :nodoc:
|
58
|
+
attr_reader :warning_count #
|
59
|
+
attr_reader :server_version #
|
60
|
+
attr_reader :protocol #
|
61
|
+
|
62
|
+
def self.new(*args, &block) # :nodoc:
|
63
|
+
my = self.allocate
|
64
|
+
my.instance_eval{initialize(*args)}
|
65
|
+
return my unless block
|
66
|
+
begin
|
67
|
+
return block.call my
|
68
|
+
ensure
|
69
|
+
my.close
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# === Return
|
74
|
+
# The value that block returns if block is specified.
|
75
|
+
# Otherwise this returns Mysql object.
|
76
|
+
def self.connect(*args, &block)
|
77
|
+
my = self.new *args
|
78
|
+
my.connect
|
79
|
+
return my unless block
|
80
|
+
begin
|
81
|
+
return block.call my
|
82
|
+
ensure
|
83
|
+
my.close
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# :call-seq:
|
88
|
+
# new(conninfo, opt={})
|
89
|
+
# new(conninfo, opt={}) {|my| ...}
|
90
|
+
#
|
91
|
+
# Connect to mysqld.
|
92
|
+
# If block is specified then the connection is closed when exiting the block.
|
93
|
+
# === Argument
|
94
|
+
# conninfo ::
|
95
|
+
# [String / URI / Hash] Connection information.
|
96
|
+
# If conninfo is String then it's format must be "mysql://user:password@hostname:port/dbname".
|
97
|
+
# If conninfo is URI then it's scheme must be "mysql".
|
98
|
+
# If conninfo is Hash then valid keys are :host, :user, :password, :db, :port, :socket and :flag.
|
99
|
+
# opt :: [Hash] options.
|
100
|
+
# === Options
|
101
|
+
# :connect_timeout :: [Numeric] The number of seconds before connection timeout.
|
102
|
+
# :init_command :: [String] Statement to execute when connecting to the MySQL server.
|
103
|
+
# :charset :: [String / Mysql::Charset] The character set to use as the default character set.
|
104
|
+
# :read_timeout :: [The timeout in seconds for attempts to read from the server.
|
105
|
+
# :write_timeout :: [Numeric] The timeout in seconds for attempts to write to the server.
|
106
|
+
# :found_rows :: [Boolean] Return the number of found (matched) rows, not the number of changed rows.
|
107
|
+
# :ignore_space :: [Boolean] Allow spaces after function names.
|
108
|
+
# :interactive :: [Boolean] Allow `interactive_timeout' seconds (instead of `wait_timeout' seconds) of inactivity before closing the connection.
|
109
|
+
# :local_files :: [Boolean] Enable `LOAD DATA LOCAL' handling.
|
110
|
+
# :no_schema :: [Boolean] Don't allow the DB_NAME.TBL_NAME.COL_NAME syntax.
|
111
|
+
# === Block parameter
|
112
|
+
# my :: [ Mysql ]
|
113
|
+
def initialize(*args)
|
114
|
+
@fields = nil
|
115
|
+
@protocol = nil
|
116
|
+
@charset = nil
|
117
|
+
@connect_timeout = nil
|
118
|
+
@read_timeout = nil
|
119
|
+
@write_timeout = nil
|
120
|
+
@init_command = nil
|
121
|
+
@affected_rows = nil
|
122
|
+
@server_version = nil
|
123
|
+
@param, opt = conninfo *args
|
124
|
+
@connected = false
|
125
|
+
set_option opt
|
126
|
+
end
|
127
|
+
|
128
|
+
def connect(*args)
|
129
|
+
param, opt = conninfo *args
|
130
|
+
set_option opt
|
131
|
+
param = @param.merge param
|
132
|
+
@protocol = Protocol.new param[:host], param[:port], param[:socket], @connect_timeout, @read_timeout, @write_timeout
|
133
|
+
@protocol.synchronize do
|
134
|
+
init_packet = @protocol.read_initial_packet
|
135
|
+
@server_version = init_packet.server_version.split(/\D/)[0,3].inject{|a,b|a.to_i*100+b.to_i}
|
136
|
+
client_flags = CLIENT_LONG_PASSWORD | CLIENT_LONG_FLAG | CLIENT_TRANSACTIONS | CLIENT_PROTOCOL_41 | CLIENT_SECURE_CONNECTION
|
137
|
+
client_flags |= CLIENT_CONNECT_WITH_DB if param[:db]
|
138
|
+
client_flags |= param[:flag] if param[:flag]
|
139
|
+
unless @charset
|
140
|
+
@charset = Charset.by_number(init_packet.server_charset)
|
141
|
+
@charset.encoding # raise error if unsupported charset
|
142
|
+
end
|
143
|
+
netpw = init_packet.crypt_password param[:password]
|
144
|
+
auth_packet = Protocol::AuthenticationPacket.new client_flags, 1024**3, @charset.number, param[:user], netpw, param[:db]
|
145
|
+
@protocol.send_packet auth_packet
|
146
|
+
@protocol.read # skip OK packet
|
147
|
+
end
|
148
|
+
@stmt_cache = Cache.new(@prepared_statement_cache_size)
|
149
|
+
simple_query @init_command if @init_command
|
150
|
+
return self
|
151
|
+
end
|
152
|
+
|
153
|
+
def close
|
154
|
+
if @protocol
|
155
|
+
@protocol.synchronize do
|
156
|
+
@protocol.send_packet Protocol::QuitPacket.new
|
157
|
+
@protocol.close
|
158
|
+
@protocol = nil
|
159
|
+
end
|
160
|
+
end
|
161
|
+
return self
|
162
|
+
end
|
163
|
+
|
164
|
+
# set characterset of MySQL connection
|
165
|
+
# === Argument
|
166
|
+
# cs :: [String / Mysql::Charset]
|
167
|
+
# === Return
|
168
|
+
# cs
|
169
|
+
def charset=(cs)
|
170
|
+
charset = cs.is_a?(Charset) ? cs : Charset.by_name(cs)
|
171
|
+
query "SET NAMES #{charset.name}" if @protocol
|
172
|
+
@charset = charset
|
173
|
+
cs
|
174
|
+
end
|
175
|
+
|
176
|
+
# Execute query string.
|
177
|
+
# If str begin with "sel" or params is specified, then the query is executed as prepared-statement automatically.
|
178
|
+
# So the values in result set are not only String.
|
179
|
+
# === Argument
|
180
|
+
# str :: [String] Query.
|
181
|
+
# params :: Parameters corresponding to place holder (`?') in str.
|
182
|
+
# === Return
|
183
|
+
# Mysql::Statement :: If result set exist when str begin with "sel".
|
184
|
+
# Mysql::Result :: If result set exist when str does not begin with "sel".
|
185
|
+
# nil :: If result set does not exist.
|
186
|
+
# === Example
|
187
|
+
# my.query("select 1,NULL,'abc'").fetch # => [1, nil, "abc"]
|
188
|
+
def query(str, *params)
|
189
|
+
if not params.empty? or str =~ /\A\s*sel/i
|
190
|
+
st = @stmt_cache.get str do |s|
|
191
|
+
prepare s
|
192
|
+
end
|
193
|
+
st.execute(*params)
|
194
|
+
if st.fields.empty?
|
195
|
+
@affected_rows = st.affected_rows
|
196
|
+
@insert_id = st.insert_id
|
197
|
+
@server_status = st.server_status
|
198
|
+
@warning_count = st.warning_count
|
199
|
+
return nil
|
200
|
+
end
|
201
|
+
return st
|
202
|
+
else
|
203
|
+
return simple_query(str)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
|
207
|
+
# Execute query string.
|
208
|
+
# The values in result set are String even if it is numeric.
|
209
|
+
# === Argument
|
210
|
+
# str :: [String] query string
|
211
|
+
# === Return
|
212
|
+
# Mysql::Result :: If result set is exist.
|
213
|
+
# nil :: If result set is not eixst.
|
214
|
+
# === Example
|
215
|
+
# my.simple_query("select 1,NULL,'abc'").fetch # => ["1", nil, "abc"]
|
216
|
+
def simple_query(str, &block)
|
217
|
+
@affected_rows = @insert_id = @server_status = @warning_count = 0
|
218
|
+
@fields = nil
|
219
|
+
@protocol.synchronize do
|
220
|
+
@protocol.reset
|
221
|
+
@protocol.send_packet Protocol::QueryPacket.new @charset.convert(str)
|
222
|
+
res_packet = @protocol.read_result_packet
|
223
|
+
if res_packet.field_count == 0
|
224
|
+
@affected_rows, @insert_id, @server_status, @warning_conut =
|
225
|
+
res_packet.affected_rows, res_packet.insert_id, res_packet.server_status, res_packet.warning_count
|
226
|
+
else
|
227
|
+
@fields = (1..res_packet.field_count).map{Field.new @protocol.read_field_packet}
|
228
|
+
@protocol.read_eof_packet
|
229
|
+
end
|
230
|
+
if block
|
231
|
+
yield Result.new(self, @fields)
|
232
|
+
return self
|
233
|
+
end
|
234
|
+
return @fields && Result.new(self, @fields)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# Parse prepared-statement.
|
239
|
+
# === Argument
|
240
|
+
# str :: [String] query string
|
241
|
+
# === Return
|
242
|
+
# Mysql::Statement :: Prepared-statement object
|
243
|
+
def prepare(str, &block)
|
244
|
+
st = Statement.new self
|
245
|
+
st.prepare str
|
246
|
+
if block
|
247
|
+
begin
|
248
|
+
return block.call st
|
249
|
+
ensure
|
250
|
+
st.close
|
251
|
+
end
|
252
|
+
end
|
253
|
+
return st
|
254
|
+
end
|
255
|
+
|
256
|
+
# Escape special character in MySQL.
|
257
|
+
# === Note
|
258
|
+
# In Ruby 1.8, this is not safe for multibyte charset such as 'SJIS'.
|
259
|
+
# You should use place-holder in prepared-statement.
|
260
|
+
def escape_string(str)
|
261
|
+
str.gsub(/[\0\n\r\\\'\"\x1a]/) do |s|
|
262
|
+
case s
|
263
|
+
when "\0" then "\\0"
|
264
|
+
when "\n" then "\\n"
|
265
|
+
when "\r" then "\\r"
|
266
|
+
when "\x1a" then "\\Z"
|
267
|
+
else "\\#{s}"
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
alias quote escape_string
|
272
|
+
|
273
|
+
# :call-seq:
|
274
|
+
# statement()
|
275
|
+
# statement() {|st| ... }
|
276
|
+
#
|
277
|
+
# Make empty prepared-statement object.
|
278
|
+
# If block is specified then prepared-statement is closed when exiting the block.
|
279
|
+
# === Block parameter
|
280
|
+
# st :: [ Mysql::Stmt ] Prepared-statement object.
|
281
|
+
# === Return
|
282
|
+
# Mysql::Statement :: If block is not specified.
|
283
|
+
# The value returned by block :: If block is specified.
|
284
|
+
def statement(&block)
|
285
|
+
st = Statement.new self
|
286
|
+
if block
|
287
|
+
begin
|
288
|
+
return block.call st
|
289
|
+
ensure
|
290
|
+
st.close
|
291
|
+
end
|
292
|
+
end
|
293
|
+
return st
|
294
|
+
end
|
295
|
+
|
296
|
+
private
|
297
|
+
|
298
|
+
# analyze argument and returns connection-parameter and option.
|
299
|
+
#
|
300
|
+
# connection-parameter's key :: :host, :user, :password, :db, :port, :socket, :flag
|
301
|
+
# === Return
|
302
|
+
# Hash :: connection parameters
|
303
|
+
# Hash :: option {:optname => value, ...}
|
304
|
+
def conninfo(*args)
|
305
|
+
paramkeys = [:host, :user, :password, :db, :port, :socket, :flag]
|
306
|
+
opt = {}
|
307
|
+
if args.empty?
|
308
|
+
param = {}
|
309
|
+
elsif args.size == 1 and args.first.is_a? Hash
|
310
|
+
arg = args.first.dup
|
311
|
+
param = {}
|
312
|
+
[:host, :user, :password, :db, :port, :socket, :flag].each do |k|
|
313
|
+
param[k] = arg.delete k if arg.key? k
|
314
|
+
end
|
315
|
+
opt = arg
|
316
|
+
else
|
317
|
+
if args.last.is_a? Hash
|
318
|
+
args = args.dup
|
319
|
+
opt = args.pop
|
320
|
+
end
|
321
|
+
if args.size > 1 || args.first.nil? || args.first.is_a?(String) && args.first !~ /\Amysql:/
|
322
|
+
host, user, password, db, port, socket, flag = args
|
323
|
+
param = {:host=>host, :user=>user, :password=>password, :db=>db, :port=>port, :socket=>socket, :flag=>flag}
|
324
|
+
elsif args.first.is_a? Hash
|
325
|
+
param = args.first.dup
|
326
|
+
param.keys.each do |k|
|
327
|
+
unless paramkeys.include? k
|
328
|
+
raise ArgumentError, "Unknown parameter: #{k.inspect}"
|
329
|
+
end
|
330
|
+
end
|
331
|
+
else
|
332
|
+
if args.first =~ /\Amysql:/
|
333
|
+
require "uri" unless defined? URI
|
334
|
+
uri = URI.parse args.first
|
335
|
+
elsif defined? URI and args.first.is_a? URI
|
336
|
+
uri = args.first
|
337
|
+
else
|
338
|
+
raise ArgumentError, "Invalid argument: #{args.first.inspect}"
|
339
|
+
end
|
340
|
+
unless uri.scheme == "mysql"
|
341
|
+
raise ArgumentError, "Invalid scheme: #{uri.scheme}"
|
342
|
+
end
|
343
|
+
param = {:host=>uri.host, :user=>uri.user, :password=>uri.password, :port=>uri.port||MYSQL_TCP_PORT}
|
344
|
+
param[:db] = uri.path.split(/\/+/).reject{|a|a.empty?}.first
|
345
|
+
if uri.query
|
346
|
+
uri.query.split(/\&/).each do |a|
|
347
|
+
k, v = a.split(/\=/, 2)
|
348
|
+
if k == "socket"
|
349
|
+
param[:socket] = v
|
350
|
+
elsif k == "flag"
|
351
|
+
param[:flag] = v.to_i
|
352
|
+
else
|
353
|
+
opt[k.intern] = v
|
354
|
+
end
|
355
|
+
end
|
356
|
+
end
|
357
|
+
end
|
358
|
+
end
|
359
|
+
param[:flag] = 0 unless param.key? :flag
|
360
|
+
opt.keys.each do |k|
|
361
|
+
if OPT2FLAG.key? k and opt[k]
|
362
|
+
param[:flag] |= OPT2FLAG[k]
|
363
|
+
next
|
364
|
+
end
|
365
|
+
unless OPTIONS.key? k
|
366
|
+
raise ArgumentError, "Unknown option: #{k.inspect}"
|
367
|
+
end
|
368
|
+
opt[k] = opt[k].to_i if OPTIONS[k] == Integer
|
369
|
+
end
|
370
|
+
return param, opt
|
371
|
+
end
|
372
|
+
|
373
|
+
private
|
374
|
+
|
375
|
+
def set_option(opt)
|
376
|
+
opt.each do |k,v|
|
377
|
+
raise ClientError, "unknown option: #{k.inspect}" unless OPTIONS.key? k
|
378
|
+
type = OPTIONS[k]
|
379
|
+
if type.is_a? Class
|
380
|
+
raise ClientError, "invalid value for #{k.inspect}: #{v.inspect}" unless v.is_a? type
|
381
|
+
end
|
382
|
+
end
|
383
|
+
|
384
|
+
charset = opt[:charset] if opt.key? :charset
|
385
|
+
@connect_timeout = opt[:connect_timeout] || @connect_timeout
|
386
|
+
@init_command = opt[:init_command] || @init_command
|
387
|
+
@read_timeout = opt[:read_timeout] || @read_timeout
|
388
|
+
@write_timeout = opt[:write_timeout] || @write_timeout
|
389
|
+
@prepared_statement_cache_size = opt[:prepared_statement_cache_size] || @prepared_statement_cache_size || 10
|
390
|
+
end
|
391
|
+
|
392
|
+
class Field
|
393
|
+
attr_reader :db, :table, :org_table, :name, :org_name, :charsetnr, :length, :type, :flags, :decimals, :default
|
394
|
+
alias :def :default
|
395
|
+
|
396
|
+
# === Argument
|
397
|
+
# packet :: [Protocol::FieldPacket]
|
398
|
+
def initialize(packet)
|
399
|
+
@db, @table, @org_table, @name, @org_name, @charsetnr, @length, @type, @flags, @decimals, @default =
|
400
|
+
packet.db, packet.table, packet.org_table, packet.name, packet.org_name, packet.charsetnr, packet.length, packet.type, packet.flags, packet.decimals, packet.default
|
401
|
+
@flags |= NUM_FLAG if is_num_type?
|
402
|
+
end
|
403
|
+
|
404
|
+
def is_num?
|
405
|
+
@flags & NUM_FLAG != 0
|
406
|
+
end
|
407
|
+
|
408
|
+
def is_not_null?
|
409
|
+
@flags & NOT_NULL_FLAG != 0
|
410
|
+
end
|
411
|
+
|
412
|
+
def is_pri_key?
|
413
|
+
@flags & PRI_KEY_FLAG != 0
|
414
|
+
end
|
415
|
+
|
416
|
+
private
|
417
|
+
|
418
|
+
def is_num_type?
|
419
|
+
[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))
|
420
|
+
end
|
421
|
+
|
422
|
+
end
|
423
|
+
|
424
|
+
class Result
|
425
|
+
|
426
|
+
include Enumerable
|
427
|
+
|
428
|
+
attr_reader :fields
|
429
|
+
|
430
|
+
def initialize(mysql, fields)
|
431
|
+
@mysql = mysql
|
432
|
+
@fields = fields
|
433
|
+
@fieldname_with_table = nil
|
434
|
+
@field_index = 0
|
435
|
+
@records = recv_all_records mysql.protocol, @fields, mysql.charset
|
436
|
+
@index = 0
|
437
|
+
end
|
438
|
+
|
439
|
+
def fetch_row
|
440
|
+
rec = @records[@index]
|
441
|
+
@index += 1 if @index < @records.length
|
442
|
+
return rec
|
443
|
+
end
|
444
|
+
alias fetch fetch_row
|
445
|
+
|
446
|
+
def fetch_hash(with_table=nil)
|
447
|
+
row = fetch_row
|
448
|
+
return nil unless row
|
449
|
+
if with_table and @fieldname_with_table.nil?
|
450
|
+
@fieldname_with_table = @fields.map{|f| [f.table, f.name].join(".")}
|
451
|
+
end
|
452
|
+
ret = {}
|
453
|
+
@fields.each_index do |i|
|
454
|
+
fname = with_table ? @fieldname_with_table[i] : @fields[i].name
|
455
|
+
ret[fname] = row[i]
|
456
|
+
end
|
457
|
+
ret
|
458
|
+
end
|
459
|
+
|
460
|
+
def each(&block)
|
461
|
+
return enum_for(:each) unless block
|
462
|
+
while rec = fetch_row
|
463
|
+
block.call rec
|
464
|
+
end
|
465
|
+
self
|
466
|
+
end
|
467
|
+
|
468
|
+
def each_hash(with_table=nil, &block)
|
469
|
+
return enum_for(:each_hash, with_table) unless block
|
470
|
+
while rec = fetch_hash(with_table)
|
471
|
+
block.call rec
|
472
|
+
end
|
473
|
+
self
|
474
|
+
end
|
475
|
+
|
476
|
+
private
|
477
|
+
|
478
|
+
def recv_all_records(protocol, fields, charset)
|
479
|
+
ret = []
|
480
|
+
while true
|
481
|
+
data = protocol.read
|
482
|
+
break if Protocol.eof_packet? data
|
483
|
+
rec = fields.map do |f|
|
484
|
+
v = Protocol.lcs2str! data
|
485
|
+
v.nil? ? nil : f.flags & Field::BINARY_FLAG == 0 ? charset.force_encoding(v) : Charset.to_binary(v)
|
486
|
+
end
|
487
|
+
ret.push rec
|
488
|
+
end
|
489
|
+
ret
|
490
|
+
end
|
491
|
+
end
|
492
|
+
|
493
|
+
class Time
|
494
|
+
def initialize(year=0, month=0, day=0, hour=0, minute=0, second=0, neg=false, second_part=0)
|
495
|
+
@year, @month, @day, @hour, @minute, @second, @neg, @second_part =
|
496
|
+
year.to_i, month.to_i, day.to_i, hour.to_i, minute.to_i, second.to_i, neg, second_part.to_i
|
497
|
+
end
|
498
|
+
attr_accessor :year, :month, :day, :hour, :minute, :second, :neg, :second_part
|
499
|
+
alias mon month
|
500
|
+
alias min minute
|
501
|
+
alias sec second
|
502
|
+
|
503
|
+
def ==(other)
|
504
|
+
other.is_a?(Mysql::Time) &&
|
505
|
+
@year == other.year && @month == other.month && @day == other.day &&
|
506
|
+
@hour == other.hour && @minute == other.minute && @second == other.second &&
|
507
|
+
@neg == neg && @second_part == other.second_part
|
508
|
+
end
|
509
|
+
|
510
|
+
def eql?(other)
|
511
|
+
self == other
|
512
|
+
end
|
513
|
+
|
514
|
+
def to_s
|
515
|
+
if year == 0 and mon == 0 and day == 0
|
516
|
+
sprintf "%02d:%02d:%02d", hour, min, sec
|
517
|
+
else
|
518
|
+
sprintf "%04d-%02d-%02d %02d:%02d:%02d", year, mon, day, hour, min, sec
|
519
|
+
end
|
520
|
+
end
|
521
|
+
|
522
|
+
end
|
523
|
+
|
524
|
+
class Statement
|
525
|
+
|
526
|
+
include Enumerable
|
527
|
+
|
528
|
+
attr_reader :affected_rows, :insert_id, :server_status, :warning_count
|
529
|
+
attr_reader :param_count, :fields, :sqlstate
|
530
|
+
attr_accessor :cursor_type
|
531
|
+
|
532
|
+
def self.finalizer(protocol, statement_id)
|
533
|
+
proc do
|
534
|
+
Thread.new do
|
535
|
+
protocol.synchronize do
|
536
|
+
protocol.reset
|
537
|
+
protocol.send_packet Protocol::StmtClosePacket.new statement_id
|
538
|
+
end
|
539
|
+
end
|
540
|
+
end
|
541
|
+
end
|
542
|
+
|
543
|
+
def initialize(mysql)
|
544
|
+
@mysql = mysql
|
545
|
+
@protocol = mysql.protocol
|
546
|
+
@statement_id = nil
|
547
|
+
@affected_rows = @insert_id = @server_status = @warning_count = 0
|
548
|
+
@eof = false
|
549
|
+
@sqlstate = "00000"
|
550
|
+
@cursor_type = CURSOR_TYPE_NO_CURSOR
|
551
|
+
@param_count = nil
|
552
|
+
end
|
553
|
+
|
554
|
+
# parse prepared-statement and return Mysql::Statement object
|
555
|
+
# === Argument
|
556
|
+
# str :: [String] query string
|
557
|
+
# === Return
|
558
|
+
# self
|
559
|
+
def prepare(str)
|
560
|
+
close
|
561
|
+
@protocol.synchronize do
|
562
|
+
begin
|
563
|
+
@sqlstate = "00000"
|
564
|
+
@protocol.reset
|
565
|
+
@protocol.send_packet Protocol::PreparePacket.new @mysql.charset.convert(str)
|
566
|
+
res_packet = @protocol.read_prepare_result_packet
|
567
|
+
if res_packet.param_count > 0
|
568
|
+
res_packet.param_count.times{@protocol.read} # skip parameter packet
|
569
|
+
@protocol.read_eof_packet
|
570
|
+
end
|
571
|
+
if res_packet.field_count > 0
|
572
|
+
fields = (1..res_packet.field_count).map{Field.new @protocol.read_field_packet}
|
573
|
+
@protocol.read_eof_packet
|
574
|
+
else
|
575
|
+
fields = []
|
576
|
+
end
|
577
|
+
@statement_id = res_packet.statement_id
|
578
|
+
@param_count = res_packet.param_count
|
579
|
+
@fields = fields
|
580
|
+
rescue ServerError => e
|
581
|
+
@sqlstate = e.sqlstate
|
582
|
+
raise
|
583
|
+
end
|
584
|
+
end
|
585
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer(@protocol, @statement_id))
|
586
|
+
self
|
587
|
+
end
|
588
|
+
|
589
|
+
def execute(*values)
|
590
|
+
raise ClientError, "not prepared" unless @param_count
|
591
|
+
raise ClientError, "parameter count mismatch" if values.length != @param_count
|
592
|
+
values = values.map{|v| @mysql.charset.convert v}
|
593
|
+
@protocol.synchronize do
|
594
|
+
begin
|
595
|
+
@sqlstate = "00000"
|
596
|
+
@protocol.reset
|
597
|
+
cursor_type = @fields.empty? ? CURSOR_TYPE_NO_CURSOR : @cursor_type
|
598
|
+
@protocol.send_packet Protocol::ExecutePacket.new @statement_id, cursor_type, values
|
599
|
+
res_packet = @protocol.read_result_packet
|
600
|
+
raise ProtocolError, "invalid field_count" unless res_packet.field_count == @fields.length
|
601
|
+
@fieldname_with_table = nil
|
602
|
+
if res_packet.field_count == 0
|
603
|
+
@affected_rows, @insert_id, @server_status, @warning_conut =
|
604
|
+
res_packet.affected_rows, res_packet.insert_id, res_packet.server_status, res_packet.warning_count
|
605
|
+
@records = nil
|
606
|
+
else
|
607
|
+
@fields = (1..res_packet.field_count).map{Field.new @protocol.read_field_packet}
|
608
|
+
@protocol.read_eof_packet
|
609
|
+
@eof = false
|
610
|
+
@index = 0
|
611
|
+
if @cursor_type == CURSOR_TYPE_NO_CURSOR
|
612
|
+
@records = []
|
613
|
+
while rec = parse_data(@protocol.read)
|
614
|
+
@records.push rec
|
615
|
+
end
|
616
|
+
end
|
617
|
+
end
|
618
|
+
return self
|
619
|
+
rescue ServerError => e
|
620
|
+
@sqlstate = e.sqlstate
|
621
|
+
raise
|
622
|
+
end
|
623
|
+
end
|
624
|
+
end
|
625
|
+
|
626
|
+
def fetch_row
|
627
|
+
return nil if @fields.empty?
|
628
|
+
if @records
|
629
|
+
rec = @records[@index]
|
630
|
+
@index += 1 if @index < @records.length
|
631
|
+
return rec
|
632
|
+
end
|
633
|
+
return nil if @eof
|
634
|
+
@protocol.synchronize do
|
635
|
+
@protocol.reset
|
636
|
+
@protocol.send_packet Protocol::FetchPacket.new @statement_id, 1
|
637
|
+
data = @protocol.read
|
638
|
+
if Protocol.eof_packet? data
|
639
|
+
@eof = true
|
640
|
+
return nil
|
641
|
+
end
|
642
|
+
@protocol.read_eof_packet
|
643
|
+
return parse_data data
|
644
|
+
end
|
645
|
+
end
|
646
|
+
alias fetch fetch_row
|
647
|
+
|
648
|
+
def fetch_hash(with_table=nil)
|
649
|
+
row = fetch_row
|
650
|
+
return nil unless row
|
651
|
+
if with_table and @fieldname_with_table.nil?
|
652
|
+
@fieldname_with_table = @fields.map{|f| [f.table, f.name].join(".")}
|
653
|
+
end
|
654
|
+
ret = {}
|
655
|
+
@fields.each_index do |i|
|
656
|
+
fname = with_table ? @fieldname_with_table[i] : @fields[i].name
|
657
|
+
ret[fname] = row[i]
|
658
|
+
end
|
659
|
+
ret
|
660
|
+
end
|
661
|
+
|
662
|
+
def each(&block)
|
663
|
+
return enum_for(:each) unless block
|
664
|
+
while rec = fetch_row
|
665
|
+
block.call rec
|
666
|
+
end
|
667
|
+
self
|
668
|
+
end
|
669
|
+
|
670
|
+
def each_hash(with_table=nil, &block)
|
671
|
+
return enum_for(:each_hash, with_table) unless block
|
672
|
+
while rec = fetch_hash(with_table)
|
673
|
+
block.call rec
|
674
|
+
end
|
675
|
+
self
|
676
|
+
end
|
677
|
+
|
678
|
+
def close
|
679
|
+
ObjectSpace.undefine_finalizer(self)
|
680
|
+
@protocol.synchronize do
|
681
|
+
@protocol.reset
|
682
|
+
if @statement_id
|
683
|
+
@protocol.send_packet Protocol::StmtClosePacket.new @statement_id
|
684
|
+
@statement_id = nil
|
685
|
+
end
|
686
|
+
end
|
687
|
+
end
|
688
|
+
|
689
|
+
private
|
690
|
+
|
691
|
+
def parse_data(data)
|
692
|
+
return nil if Protocol.eof_packet? data
|
693
|
+
data.slice!(0) # skip first byte
|
694
|
+
null_bit_map = data.slice!(0, (@fields.length+7+2)/8).unpack("C*")
|
695
|
+
ret = (0...@fields.length).map do |i|
|
696
|
+
if null_bit_map[(i+2)/8][(i+2)%8] == 1
|
697
|
+
nil
|
698
|
+
else
|
699
|
+
unsigned = @fields[i].flags & Field::UNSIGNED_FLAG != 0
|
700
|
+
v = Protocol.net2value(data, @fields[i].type, unsigned)
|
701
|
+
@fields[i].flags & Field::BINARY_FLAG == 0 ? @mysql.charset.force_encoding(v) : Charset.to_binary(v)
|
702
|
+
end
|
703
|
+
end
|
704
|
+
ret
|
705
|
+
end
|
706
|
+
|
707
|
+
end
|
708
|
+
end
|