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.
@@ -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