tmtm-ruby-mysql 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,3 @@
1
+ == 3.0.0 / 2009-03-22
2
+
3
+ * initial release for v3.0.
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
@@ -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
@@ -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