tmtm-ruby-mysql 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|