ruby-mysql 3.0.0 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +58 -0
- data/lib/mysql/authenticator/caching_sha2_password.rb +3 -2
- data/lib/mysql/authenticator/mysql_native_password.rb +1 -0
- data/lib/mysql/authenticator/sha256_password.rb +1 -0
- data/lib/mysql/authenticator.rb +6 -4
- data/lib/mysql/charset.rb +6 -6
- data/lib/mysql/constants.rb +8 -0
- data/lib/mysql/error.rb +7 -5
- data/lib/mysql/field.rb +95 -0
- data/lib/mysql/packet.rb +6 -5
- data/lib/mysql/protocol.rb +296 -189
- data/lib/mysql/result.rb +212 -0
- data/lib/mysql/stmt.rb +219 -0
- data/lib/mysql.rb +109 -523
- metadata +57 -15
- data/test/test_mysql.rb +0 -1566
- data/test/test_mysql_packet.rb +0 -149
data/lib/mysql/result.rb
ADDED
@@ -0,0 +1,212 @@
|
|
1
|
+
class Mysql
|
2
|
+
# @!visibility public
|
3
|
+
# Result set
|
4
|
+
class ResultBase
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
# @return [Array<Mysql::Field>] field list
|
8
|
+
attr_reader :fields
|
9
|
+
alias fetch_fields fields
|
10
|
+
|
11
|
+
# @return [Mysql::StatementResult]
|
12
|
+
attr_reader :result
|
13
|
+
|
14
|
+
# @param [Array of Mysql::Field] fields
|
15
|
+
def initialize(fields, protocol, record_class, **opts)
|
16
|
+
@fields = fields
|
17
|
+
@field_index = 0 # index of field
|
18
|
+
@records = [] # all records
|
19
|
+
@index = 0 # index of record
|
20
|
+
@fieldname_with_table = nil
|
21
|
+
@protocol = protocol
|
22
|
+
@record_class = record_class
|
23
|
+
@opts = opts
|
24
|
+
end
|
25
|
+
|
26
|
+
def retrieve
|
27
|
+
@records = @protocol.retr_all_records(@record_class)
|
28
|
+
end
|
29
|
+
|
30
|
+
# ignore
|
31
|
+
# @return [void]
|
32
|
+
def free
|
33
|
+
# dummy
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [Integer] number of record
|
37
|
+
def size
|
38
|
+
@records.size
|
39
|
+
end
|
40
|
+
alias num_rows size
|
41
|
+
|
42
|
+
# @return [Array] current record data
|
43
|
+
def fetch(**)
|
44
|
+
if @index < @records.size
|
45
|
+
@records[@index] = @records[@index].to_a unless @records[@index].is_a? Array
|
46
|
+
@index += 1
|
47
|
+
return @records[@index-1]
|
48
|
+
end
|
49
|
+
rec = @protocol.retr_record(@record_class)&.to_a
|
50
|
+
return nil unless rec
|
51
|
+
@records[@index] = rec
|
52
|
+
@index += 1
|
53
|
+
return rec
|
54
|
+
end
|
55
|
+
alias fetch_row fetch
|
56
|
+
|
57
|
+
# Return data of current record as Hash.
|
58
|
+
# The hash key is field name.
|
59
|
+
# @param [Boolean] with_table if true, hash key is "table_name.field_name".
|
60
|
+
# @return [Hash] current record data
|
61
|
+
def fetch_hash(**opts)
|
62
|
+
row = fetch(**opts)
|
63
|
+
return nil unless row
|
64
|
+
with_table = @opts.merge(opts)[:with_table]
|
65
|
+
if with_table and @fieldname_with_table.nil?
|
66
|
+
@fieldname_with_table = @fields.map{|f| [f.table, f.name].join(".")}
|
67
|
+
end
|
68
|
+
ret = {}
|
69
|
+
@fields.each_index do |i|
|
70
|
+
fname = with_table ? @fieldname_with_table[i] : @fields[i].name
|
71
|
+
ret[fname] = row[i]
|
72
|
+
end
|
73
|
+
ret
|
74
|
+
end
|
75
|
+
|
76
|
+
# Iterate block with record.
|
77
|
+
# @yield [Array] record data
|
78
|
+
# @return [self] self. If block is not specified, this returns Enumerator.
|
79
|
+
def each(**opts, &block)
|
80
|
+
@index = 0
|
81
|
+
return enum_for(:each, **opts) unless block
|
82
|
+
while (rec = fetch(**opts))
|
83
|
+
block.call rec
|
84
|
+
end
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
# Iterate block with record as Hash.
|
89
|
+
# @param [Boolean] with_table if true, hash key is "table_name.field_name".
|
90
|
+
# @yield [Hash] record data
|
91
|
+
# @return [self] self. If block is not specified, this returns Enumerator.
|
92
|
+
def each_hash(**opts, &block)
|
93
|
+
@index = 0
|
94
|
+
return enum_for(:each_hash, **opts) unless block
|
95
|
+
while (rec = fetch_hash(**opts))
|
96
|
+
block.call rec
|
97
|
+
end
|
98
|
+
self
|
99
|
+
end
|
100
|
+
|
101
|
+
# Set record position
|
102
|
+
# @param [Integer] n record index
|
103
|
+
# @return [self] self
|
104
|
+
def data_seek(n)
|
105
|
+
@index = n
|
106
|
+
self
|
107
|
+
end
|
108
|
+
|
109
|
+
# @return [Integer] current record position
|
110
|
+
def row_tell
|
111
|
+
@index
|
112
|
+
end
|
113
|
+
|
114
|
+
# Set current position of record
|
115
|
+
# @param [Integer] n record index
|
116
|
+
# @return [Integer] previous position
|
117
|
+
def row_seek(n)
|
118
|
+
ret = @index
|
119
|
+
@index = n
|
120
|
+
ret
|
121
|
+
end
|
122
|
+
|
123
|
+
# Server status value
|
124
|
+
# @return [Integer] server status value
|
125
|
+
def server_status
|
126
|
+
@protocol.server_status
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
# @!visibility public
|
131
|
+
# Result set for simple query
|
132
|
+
class Result < ResultBase
|
133
|
+
# @private
|
134
|
+
# @param [Array<Mysql::Field>] fields
|
135
|
+
# @param [Mysql::Protocol] protocol
|
136
|
+
# @param [Boolean] auto_store_result
|
137
|
+
def initialize(fields, protocol=nil, **opts)
|
138
|
+
super fields, protocol, RawRecord, **opts
|
139
|
+
return unless protocol
|
140
|
+
fields.each{|f| f.result = self} # for calculating max_field
|
141
|
+
retrieve if @opts.merge(opts)[:auto_store_result]
|
142
|
+
end
|
143
|
+
|
144
|
+
def fetch(**opts)
|
145
|
+
rec = super
|
146
|
+
rec = rec.map.with_index{|s, i| convert_type(@fields[i], s)} if rec && @opts.merge(opts)[:cast]
|
147
|
+
rec
|
148
|
+
end
|
149
|
+
alias fetch_row fetch
|
150
|
+
|
151
|
+
# @private
|
152
|
+
# calculate max_length of all fields
|
153
|
+
def calculate_field_max_length
|
154
|
+
return unless @records
|
155
|
+
max_length = Array.new(@fields.size, 0)
|
156
|
+
@records.each_with_index do |rec, i|
|
157
|
+
rec = @records[i] = rec.to_a if rec.is_a? RawRecord
|
158
|
+
max_length.each_index do |j|
|
159
|
+
max_length[j] = rec[j].to_s.length if rec[j] && rec[j].to_s.length > max_length[j]
|
160
|
+
end
|
161
|
+
end
|
162
|
+
max_length.each_with_index do |len, i|
|
163
|
+
@fields[i].max_length = len
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
private
|
168
|
+
|
169
|
+
def convert_type(f, s)
|
170
|
+
return nil if s.nil?
|
171
|
+
case f.type
|
172
|
+
when Field::TYPE_STRING, Field::TYPE_VAR_STRING, Field::TYPE_BLOB, Field::TYPE_JSON
|
173
|
+
s
|
174
|
+
when Field::TYPE_NEWDECIMAL
|
175
|
+
s =~ /\./ && s !~ /\.0*\z/ ? BigDecimal(s) : s.to_i
|
176
|
+
when Field::TYPE_TINY, Field::TYPE_SHORT, Field::TYPE_INT24, Field::TYPE_LONG, Field::TYPE_LONGLONG
|
177
|
+
s.to_i
|
178
|
+
when Field::TYPE_FLOAT, Field::TYPE_DOUBLE
|
179
|
+
s.to_f
|
180
|
+
when Field::TYPE_DATE
|
181
|
+
Date.parse(s) rescue nil
|
182
|
+
when Field::TYPE_DATETIME, Field::TYPE_TIMESTAMP
|
183
|
+
Time.parse(s) rescue nil
|
184
|
+
when Field::TYPE_TIME
|
185
|
+
h, m, sec = s.split(/:/)
|
186
|
+
if s =~ /\A-/
|
187
|
+
h.to_i*3600 - m.to_i*60 - sec.to_f
|
188
|
+
else
|
189
|
+
h.to_i*3600 + m.to_i*60 + sec.to_f
|
190
|
+
end
|
191
|
+
when Field::TYPE_YEAR
|
192
|
+
s.to_i
|
193
|
+
when Field::TYPE_BIT
|
194
|
+
s.b
|
195
|
+
else
|
196
|
+
s.b
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
# @!visibility private
|
202
|
+
# Result set for prepared statement
|
203
|
+
class StatementResult < ResultBase
|
204
|
+
# @private
|
205
|
+
# @param [Array<Mysql::Field>] fields
|
206
|
+
# @param [Mysql::Protocol] protocol
|
207
|
+
def initialize(fields, protocol, **opts)
|
208
|
+
super fields, protocol, StmtRawRecord, **opts
|
209
|
+
retrieve if @opts.merge(opts)[:auto_store_result]
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
data/lib/mysql/stmt.rb
ADDED
@@ -0,0 +1,219 @@
|
|
1
|
+
class Mysql
|
2
|
+
# @!visibility public
|
3
|
+
# Prepared statement
|
4
|
+
# @!attribute [r] affected_rows
|
5
|
+
# @return [Integer]
|
6
|
+
# @!attribute [r] insert_id
|
7
|
+
# @return [Integer]
|
8
|
+
# @!attribute [r] server_status
|
9
|
+
# @return [Integer]
|
10
|
+
# @!attribute [r] warning_count
|
11
|
+
# @return [Integer]
|
12
|
+
# @!attribute [r] param_count
|
13
|
+
# @return [Integer]
|
14
|
+
# @!attribute [r] fields
|
15
|
+
# @return [Array<Mysql::Field>]
|
16
|
+
# @!attribute [r] sqlstate
|
17
|
+
# @return [String]
|
18
|
+
class Stmt
|
19
|
+
include Enumerable
|
20
|
+
|
21
|
+
attr_reader :affected_rows, :info, :insert_id, :server_status, :warning_count
|
22
|
+
attr_reader :param_count, :fields, :sqlstate
|
23
|
+
|
24
|
+
# @private
|
25
|
+
def self.finalizer(protocol, statement_id)
|
26
|
+
proc do
|
27
|
+
protocol.gc_stmt statement_id
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# @private
|
32
|
+
# @param [Mysql::Protocol] protocol
|
33
|
+
def initialize(protocol, **opts)
|
34
|
+
@protocol = protocol
|
35
|
+
@opts = opts
|
36
|
+
@statement_id = nil
|
37
|
+
@affected_rows = @insert_id = @server_status = @warning_count = 0
|
38
|
+
@sqlstate = "00000"
|
39
|
+
@param_count = nil
|
40
|
+
end
|
41
|
+
|
42
|
+
# @private
|
43
|
+
# parse prepared-statement and return {Mysql::Stmt} object
|
44
|
+
# @param [String] str query string
|
45
|
+
# @return self
|
46
|
+
def prepare(str)
|
47
|
+
raise ClientError, 'MySQL client is not connected' unless @protocol
|
48
|
+
close
|
49
|
+
begin
|
50
|
+
@sqlstate = "00000"
|
51
|
+
@statement_id, @param_count, @fields = @protocol.stmt_prepare_command(str)
|
52
|
+
rescue ServerError => e
|
53
|
+
@last_error = e
|
54
|
+
@sqlstate = e.sqlstate
|
55
|
+
raise
|
56
|
+
end
|
57
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer(@protocol, @statement_id))
|
58
|
+
self
|
59
|
+
end
|
60
|
+
|
61
|
+
# Execute prepared statement.
|
62
|
+
# @param [Object] values values passed to query
|
63
|
+
# @return [Mysql::Result] if return_result is true and the query returns result set.
|
64
|
+
# @return [nil] if return_result is true and the query does not return result set.
|
65
|
+
# @return [self] if return_result is false or block is specified.
|
66
|
+
def execute(*values, **opts, &block)
|
67
|
+
raise ClientError, "Invalid statement handle" unless @statement_id
|
68
|
+
raise ClientError, "not prepared" unless @param_count
|
69
|
+
raise ClientError, "parameter count mismatch" if values.length != @param_count
|
70
|
+
values = values.map{|v| @protocol.charset.convert v}
|
71
|
+
opts = @opts.merge(opts)
|
72
|
+
begin
|
73
|
+
@sqlstate = "00000"
|
74
|
+
@protocol.stmt_execute_command @statement_id, values
|
75
|
+
@fields = @result = nil
|
76
|
+
if block
|
77
|
+
while true
|
78
|
+
get_result
|
79
|
+
res = store_result(**opts)
|
80
|
+
block.call res if res || opts[:yield_null_result]
|
81
|
+
break unless more_results?
|
82
|
+
end
|
83
|
+
return self
|
84
|
+
end
|
85
|
+
get_result
|
86
|
+
return self unless opts[:return_result]
|
87
|
+
return store_result(**opts)
|
88
|
+
rescue ServerError => e
|
89
|
+
@last_error = e
|
90
|
+
@sqlstate = e.sqlstate
|
91
|
+
raise
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def get_result
|
96
|
+
@protocol.get_result
|
97
|
+
@affected_rows, @insert_id, @server_status, @warning_count, @info =
|
98
|
+
@protocol.affected_rows, @protocol.insert_id, @protocol.server_status, @protocol.warning_count, @protocol.message
|
99
|
+
end
|
100
|
+
|
101
|
+
def store_result(**opts)
|
102
|
+
return nil if @protocol.field_count.nil? || @protocol.field_count == 0
|
103
|
+
@fields = @protocol.retr_fields
|
104
|
+
opts = @opts.merge(opts)
|
105
|
+
@result = StatementResult.new(@fields, @protocol, **opts)
|
106
|
+
end
|
107
|
+
|
108
|
+
def more_results?
|
109
|
+
@protocol.more_results?
|
110
|
+
end
|
111
|
+
|
112
|
+
# execute next query if precedure is called.
|
113
|
+
# @return [Mysql::StatementResult] result set of query if return_result is true.
|
114
|
+
# @return [true] if return_result is false and result exists.
|
115
|
+
# @return [nil] query returns no results or no more results.
|
116
|
+
def next_result(**opts)
|
117
|
+
return nil unless more_results?
|
118
|
+
opts = @opts.merge(opts)
|
119
|
+
@fields = @result = nil
|
120
|
+
get_result
|
121
|
+
return self unless opts[:return_result]
|
122
|
+
return store_result(**opts)
|
123
|
+
rescue ServerError => e
|
124
|
+
@last_error = e
|
125
|
+
@sqlstate = e.sqlstate
|
126
|
+
raise
|
127
|
+
end
|
128
|
+
|
129
|
+
# Close prepared statement
|
130
|
+
# @return [void]
|
131
|
+
def close
|
132
|
+
ObjectSpace.undefine_finalizer(self)
|
133
|
+
@protocol.stmt_close_command @statement_id if @statement_id
|
134
|
+
@statement_id = nil
|
135
|
+
end
|
136
|
+
|
137
|
+
# @return [Array] current record data
|
138
|
+
def fetch(**opts)
|
139
|
+
@result.fetch(**opts)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Return data of current record as Hash.
|
143
|
+
# The hash key is field name.
|
144
|
+
# @param [Boolean] with_table if true, hash key is "table_name.field_name".
|
145
|
+
# @return [Hash] record data
|
146
|
+
def fetch_hash(**opts)
|
147
|
+
@result.fetch_hash(**opts)
|
148
|
+
end
|
149
|
+
|
150
|
+
# Iterate block with record.
|
151
|
+
# @yield [Array] record data
|
152
|
+
# @return [Mysql::Stmt] self
|
153
|
+
# @return [Enumerator] If block is not specified
|
154
|
+
def each(**opts, &block)
|
155
|
+
return enum_for(:each, **opts) unless block
|
156
|
+
while (rec = fetch(*opts))
|
157
|
+
block.call rec
|
158
|
+
end
|
159
|
+
self
|
160
|
+
end
|
161
|
+
|
162
|
+
# Iterate block with record as Hash.
|
163
|
+
# @param [Boolean] with_table if true, hash key is "table_name.field_name".
|
164
|
+
# @yield [Hash] record data
|
165
|
+
# @return [Mysql::Stmt] self
|
166
|
+
# @return [Enumerator] If block is not specified
|
167
|
+
def each_hash(**opts, &block)
|
168
|
+
return enum_for(:each_hash, **opts) unless block
|
169
|
+
while (rec = fetch_hash(**opts))
|
170
|
+
block.call rec
|
171
|
+
end
|
172
|
+
self
|
173
|
+
end
|
174
|
+
|
175
|
+
# @return [Integer] number of record
|
176
|
+
def size
|
177
|
+
@result.size
|
178
|
+
end
|
179
|
+
alias num_rows size
|
180
|
+
|
181
|
+
# Set record position
|
182
|
+
# @param [Integer] n record index
|
183
|
+
# @return [void]
|
184
|
+
def data_seek(n)
|
185
|
+
@result.data_seek(n)
|
186
|
+
end
|
187
|
+
|
188
|
+
# @return [Integer] current record position
|
189
|
+
def row_tell
|
190
|
+
@result.row_tell
|
191
|
+
end
|
192
|
+
|
193
|
+
# Set current position of record
|
194
|
+
# @param [Integer] n record index
|
195
|
+
# @return [Integer] previous position
|
196
|
+
def row_seek(n)
|
197
|
+
@result.row_seek(n)
|
198
|
+
end
|
199
|
+
|
200
|
+
# @return [Integer] number of columns for last query
|
201
|
+
def field_count
|
202
|
+
@fields.length
|
203
|
+
end
|
204
|
+
|
205
|
+
# ignore
|
206
|
+
# @return [void]
|
207
|
+
def free_result
|
208
|
+
# dummy
|
209
|
+
end
|
210
|
+
|
211
|
+
# Returns Mysql::Result object that is empty.
|
212
|
+
# Use fields to get list of fields.
|
213
|
+
# @return [Mysql::Result]
|
214
|
+
def result_metadata
|
215
|
+
return nil if @fields.empty?
|
216
|
+
Result.new @fields
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|