ffi-mysql 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.rdoc +4 -0
- data/README +55 -0
- data/README.rdoc +55 -0
- data/Rakefile +27 -0
- data/lib/ffi-mysql.rb +49 -0
- data/lib/mysql/constants.rb +166 -0
- data/lib/mysql/error.rb +4 -0
- data/lib/mysql/field.rb +89 -0
- data/lib/mysql/mysql.rb +365 -0
- data/lib/mysql/result.rb +168 -0
- data/lib/mysql/stmt.rb +416 -0
- data/lib/mysql/time.rb +33 -0
- data/test/test_mysql.rb +1483 -0
- metadata +103 -0
data/lib/mysql/stmt.rb
ADDED
@@ -0,0 +1,416 @@
|
|
1
|
+
class Mysql
|
2
|
+
# A MySQL statement.
|
3
|
+
class Stmt
|
4
|
+
class C::Time < FFI::Struct
|
5
|
+
layout(:year, :uint,
|
6
|
+
:month, :uint,
|
7
|
+
:day, :uint,
|
8
|
+
:hour, :uint,
|
9
|
+
:minute, :uint,
|
10
|
+
:second, :uint,
|
11
|
+
:second_part, :ulong,
|
12
|
+
:neg, :uchar,
|
13
|
+
:time_type, :int)
|
14
|
+
end
|
15
|
+
|
16
|
+
class C::Bind < FFI::Struct
|
17
|
+
layout(:length, :pointer, # output length pointer
|
18
|
+
:is_null, :pointer, # Pointer to null indicator
|
19
|
+
:buffer, :pointer, # buffer to get/put data
|
20
|
+
# set this if you want to track data truncations happened during fetch
|
21
|
+
:error, :pointer,
|
22
|
+
:buffer_type, :int, # buffer type
|
23
|
+
# output buffer length, must be set when fetching str/binary
|
24
|
+
:buffer_length, :ulong,
|
25
|
+
:row_ptr, :pointer, # for the current data position
|
26
|
+
:offset, :ulong, # offset position for char/binary fetch
|
27
|
+
:length_value, :ulong, # Used if length is 0
|
28
|
+
:param_number, :uint, # For null count and error messages
|
29
|
+
:pack_length, :uint, # Internal length for packed data
|
30
|
+
:error_value, :char, # used if error is 0
|
31
|
+
:is_unsigned, :char, # set if integer type is unsigned
|
32
|
+
:long_data_used, :char, # If used with mysql_send_long_data
|
33
|
+
:is_null_value, :char, # Used if is_null is 0
|
34
|
+
:store_param_func, :pointer,
|
35
|
+
:fetch_result, :pointer,
|
36
|
+
:skip_result, :pointer)
|
37
|
+
|
38
|
+
def prepare_result( field )
|
39
|
+
self[:buffer_type] = field.type
|
40
|
+
self[:is_null] = @is_null = FFI::MemoryPointer.new(:int)
|
41
|
+
self[:length] = @buflen = FFI::MemoryPointer.new(:ulong)
|
42
|
+
self[:is_unsigned] = (field.flags & Field::UNSIGNED_FLAG) != 0 ? 1 : 0
|
43
|
+
end
|
44
|
+
|
45
|
+
def set_result( field )
|
46
|
+
case self[:buffer_type]
|
47
|
+
when Field::TYPE_NULL
|
48
|
+
nil
|
49
|
+
when Field::TYPE_TINY, Field::TYPE_SHORT, Field::TYPE_YEAR, Field::TYPE_INT24,
|
50
|
+
Field::TYPE_LONG, Field::TYPE_LONGLONG, Field::TYPE_FLOAT, Field::TYPE_DOUBLE
|
51
|
+
self[:buffer] = @buf = FFI::MemoryPointer.new(8, 1, true)
|
52
|
+
self[:buffer_length] = 8
|
53
|
+
when Field::TYPE_DECIMAL, Field::TYPE_STRING, Field::TYPE_VAR_STRING, Field::TYPE_TINY_BLOB,
|
54
|
+
Field::TYPE_BLOB, Field::TYPE_MEDIUM_BLOB, Field::TYPE_LONG_BLOB, Field::TYPE_NEWDECIMAL,
|
55
|
+
Field::TYPE_BIT
|
56
|
+
self[:buffer] = @buf = FFI::MemoryPointer.new( field.max_length, 1, true )
|
57
|
+
self[:buffer_length] = field.max_length
|
58
|
+
when Field::TYPE_TIME, Field::TYPE_DATE, Field::TYPE_DATETIME, Field::TYPE_TIMESTAMP
|
59
|
+
self[:buffer] = @buf = FFI::MemoryPointer.new( C::Time.size, 1, true )
|
60
|
+
self[:buffer_length] = C::Time.size
|
61
|
+
else
|
62
|
+
raise TypeError, "unknown buffer_type: #{self[:buffer_type]}"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def set_bind_result( arg, field )
|
67
|
+
if arg.nil? or arg == NilClass
|
68
|
+
self[:buffer_type] = field.type
|
69
|
+
elsif arg == String
|
70
|
+
self[:buffer_type] = Field::TYPE_STRING
|
71
|
+
elsif arg == Numeric or arg == Integer or arg == Fixnum
|
72
|
+
self[:buffer_type] = Field::TYPE_LONGLONG
|
73
|
+
elsif arg == Float
|
74
|
+
self[:buffer_type] = Field::TYPE_DOUBLE
|
75
|
+
elsif arg == Mysql::Time or arg == Time
|
76
|
+
self[:buffer_type] = Field::TYPE_DATETIME
|
77
|
+
else
|
78
|
+
raise TypeError, "unrecognized class: #{arg.class}"
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def set_param( arg )
|
83
|
+
case arg
|
84
|
+
when nil
|
85
|
+
self[:buffer_type] = Field::TYPE_NULL
|
86
|
+
when Fixnum
|
87
|
+
self[:buffer_type] = Field::TYPE_LONG
|
88
|
+
self[:buffer] = @buf = FFI::MemoryPointer.new( :long )
|
89
|
+
@buf.write_int(arg)
|
90
|
+
when Bignum
|
91
|
+
self[:buffer_type] = Field::TYPE_LONGLONG
|
92
|
+
self[:buffer] = @buf = FFI::MemoryPointer.new( :long_long )
|
93
|
+
@buf.write_long_long(arg)
|
94
|
+
when Float
|
95
|
+
self[:buffer_type] = Field::TYPE_DOUBLE
|
96
|
+
self[:buffer] = @buf = FFI::MemoryPointer.new( :double )
|
97
|
+
@buf.write_double(arg)
|
98
|
+
when String
|
99
|
+
self[:buffer_type] = Field::TYPE_STRING
|
100
|
+
self[:buffer] = @buf = FFI::MemoryPointer::from_string(arg)
|
101
|
+
self[:buffer_length] = arg.size
|
102
|
+
self[:length] = @buflen = FFI::MemoryPointer.new( :ulong )
|
103
|
+
@buflen.write_long( arg.size )
|
104
|
+
when ::Time, Mysql::Time
|
105
|
+
self[:buffer_type] = Field::TYPE_DATETIME
|
106
|
+
self[:buffer] = @buf = FFI::MemoryPointer.new( C::Time, 1, true )
|
107
|
+
self[:buffer_length] = C::Time.size
|
108
|
+
self[:length] = @buflen = FFI::MemoryPointer.new( :ulong )
|
109
|
+
@buflen.write_long(C::Time.size)
|
110
|
+
time = C::Time.new(@buf)
|
111
|
+
time[:second_part] = 0
|
112
|
+
time[:neg] = 0
|
113
|
+
if arg.kind_of? Mysql::Time
|
114
|
+
time[:second] = arg.second
|
115
|
+
time[:minute] = arg.minute
|
116
|
+
time[:hour] = arg.hour
|
117
|
+
time[:day] = arg.day
|
118
|
+
time[:month] = arg.month
|
119
|
+
time[:year] = arg.year
|
120
|
+
else
|
121
|
+
time[:second] = arg.sec
|
122
|
+
time[:minute] = arg.min
|
123
|
+
time[:hour] = arg.hour
|
124
|
+
time[:day] = arg.day
|
125
|
+
time[:month] = arg.month
|
126
|
+
time[:year] = arg.year
|
127
|
+
end
|
128
|
+
else
|
129
|
+
raise TypeError, "unsupported type: #{arg.class}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def value
|
134
|
+
if @is_null.read_int != 0
|
135
|
+
nil
|
136
|
+
else
|
137
|
+
case self[:buffer_type]
|
138
|
+
when Field::TYPE_TINY
|
139
|
+
b = @buf.read_long
|
140
|
+
if self[:is_unsigned] == 0 and b >= 2**7
|
141
|
+
b -= 2**8
|
142
|
+
end
|
143
|
+
b
|
144
|
+
when Field::TYPE_SHORT, Field::TYPE_YEAR
|
145
|
+
b = @buf.read_long
|
146
|
+
if self[:is_unsigned] == 0 and b >= 2**15
|
147
|
+
b -= 2**16
|
148
|
+
end
|
149
|
+
b
|
150
|
+
when Field::TYPE_INT24, Field::TYPE_LONG
|
151
|
+
if self[:is_unsigned] != 0
|
152
|
+
@buf.read_long % (2**32)
|
153
|
+
else
|
154
|
+
@buf.read_long
|
155
|
+
end
|
156
|
+
when Field::TYPE_LONGLONG
|
157
|
+
if self[:is_unsigned] != 0
|
158
|
+
@buf.read_long_long % (2**64)
|
159
|
+
else
|
160
|
+
@buf.read_long_long
|
161
|
+
end
|
162
|
+
when Field::TYPE_FLOAT
|
163
|
+
@buf.read_float
|
164
|
+
when Field::TYPE_DOUBLE
|
165
|
+
# FIXME currently not supported
|
166
|
+
@buf.read_double
|
167
|
+
when Field::TYPE_TIME, Field::TYPE_DATE, Field::TYPE_DATETIME, Field::TYPE_TIMESTAMP
|
168
|
+
time = C::Time.new(@buf)
|
169
|
+
Time.new( time[:year], time[:month], time[:day],
|
170
|
+
time[:hour], time[:minute], time[:second],
|
171
|
+
time[:neg] != 0, time[:second_part] )
|
172
|
+
when Field::TYPE_DECIMAL, Field::TYPE_STRING, Field::TYPE_VAR_STRING,
|
173
|
+
Field::TYPE_TINY_BLOB, Field::TYPE_BLOB, Field::TYPE_MEDIUM_BLOB,
|
174
|
+
Field::TYPE_LONG_BLOB, Field::TYPE_NEWDECIMAL, Field::TYPE_BIT
|
175
|
+
@buf.read_string( @buflen.read_long % (2**32) )
|
176
|
+
else
|
177
|
+
raise TypeError, "unknown buffer type: #{self[:buffer_type]}"
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def initialize( mysql )
|
184
|
+
@mysql = mysql
|
185
|
+
@stmt = C::mysql_stmt_init(mysql)
|
186
|
+
@params = []
|
187
|
+
raise Error, error_msg if @stmt.null?
|
188
|
+
ObjectSpace.define_finalizer( self, Stmt.finalizer(@stmt) )
|
189
|
+
|
190
|
+
_true = FFI::MemoryPointer.new(:int)
|
191
|
+
_true.write_int(1)
|
192
|
+
if C::mysql_stmt_attr_set(@stmt, :update_max_length, _true) != 0
|
193
|
+
raise Error, error_msg
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
# Closes the statement.
|
198
|
+
def close
|
199
|
+
C::mysql_stmt_close(@stmt)
|
200
|
+
@stmt = nil
|
201
|
+
ObjectSpace.undefine_finalizer(self)
|
202
|
+
end
|
203
|
+
|
204
|
+
# Preparse the statement
|
205
|
+
# @param [String] stmt the SQL statement
|
206
|
+
# @return [self]
|
207
|
+
def prepare(stmt)
|
208
|
+
raise Error, "Statment already closed" unless @stmt
|
209
|
+
if C::mysql_stmt_prepare(@stmt, stmt, stmt.size) != 0
|
210
|
+
raise Error, error_msg
|
211
|
+
end
|
212
|
+
|
213
|
+
nparams = param_count
|
214
|
+
@param_binds_ary = FFI::MemoryPointer.new C::Bind, nparams, true
|
215
|
+
@param_binds = (0...nparams).map {|i|
|
216
|
+
C::Bind.new(FFI::Pointer.new(C::Bind, @param_binds_ary.address + i * C::Bind.size))
|
217
|
+
}
|
218
|
+
|
219
|
+
if @result = result_metadata
|
220
|
+
fields = @result.fetch_fields
|
221
|
+
@result_binds_ary = FFI::MemoryPointer.new C::Bind, fields.size, true
|
222
|
+
@result_binds = (0...fields.size).map{|i|
|
223
|
+
bind = C::Bind.new(FFI::Pointer.new(C::Bind, @result_binds_ary.address + i * C::Bind.size))
|
224
|
+
bind.prepare_result( fields[i] )
|
225
|
+
bind
|
226
|
+
}
|
227
|
+
else
|
228
|
+
@result_binds_ary = nil
|
229
|
+
@result_binds = []
|
230
|
+
end
|
231
|
+
|
232
|
+
self
|
233
|
+
end
|
234
|
+
|
235
|
+
# Executes the statement.
|
236
|
+
# @param args statement parameters
|
237
|
+
# @return [self]
|
238
|
+
def execute(*args)
|
239
|
+
raise Error, "Statment already closed" unless @stmt
|
240
|
+
if args.size != @param_binds.size
|
241
|
+
raise Error, "param_count(#{@param_binds.size}) != number of arguments(#{args.size})"
|
242
|
+
end
|
243
|
+
|
244
|
+
free_result
|
245
|
+
unless @param_binds.empty?
|
246
|
+
@param_binds.each_with_index do |bind, i|
|
247
|
+
bind.set_param args[i]
|
248
|
+
end
|
249
|
+
if C::mysql_stmt_bind_param(@stmt, @param_binds_ary) != 0
|
250
|
+
raise Error, error_msg
|
251
|
+
end
|
252
|
+
end
|
253
|
+
|
254
|
+
if C::mysql_stmt_execute(@stmt) != 0
|
255
|
+
raise Error, error_msg
|
256
|
+
end
|
257
|
+
|
258
|
+
if @result
|
259
|
+
if C::mysql_stmt_store_result(@stmt) != 0
|
260
|
+
raise Error, error_msg
|
261
|
+
end
|
262
|
+
|
263
|
+
fields = @result.fetch_fields
|
264
|
+
@result_binds.each_with_index do |bind, i|
|
265
|
+
bind.set_result fields[i]
|
266
|
+
end
|
267
|
+
|
268
|
+
if C::mysql_stmt_bind_result(@stmt, @result_binds_ary) != 0
|
269
|
+
raise Error, error_msg
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
self
|
274
|
+
end
|
275
|
+
|
276
|
+
# Associated output columns with buffer.
|
277
|
+
# @param [Class] classes of the result columns like Fixnum, String, ...
|
278
|
+
# @return [self]
|
279
|
+
def bind_result(*args)
|
280
|
+
raise Error, "Statment already closed" unless @stmt
|
281
|
+
if args.size != @result_binds.size
|
282
|
+
raise Error, "result value count (#{@result_binds.size}) != number of arguments (#{args.size})"
|
283
|
+
end
|
284
|
+
|
285
|
+
fields = @result.fetch_fields
|
286
|
+
@result_binds.each_with_index do |bind, i|
|
287
|
+
bind.set_bind_result( args[i], fields[i] )
|
288
|
+
if C::mysql_stmt_bind_result( @stmt, @result_binds_ary ) != 0
|
289
|
+
raise Error, error_msg
|
290
|
+
end
|
291
|
+
end
|
292
|
+
self
|
293
|
+
end
|
294
|
+
|
295
|
+
# @return [Array] the next row.
|
296
|
+
def fetch
|
297
|
+
raise Error, "Statment already closed" unless @stmt
|
298
|
+
r = C::mysql_stmt_fetch(@stmt)
|
299
|
+
case r
|
300
|
+
when NO_DATA
|
301
|
+
nil
|
302
|
+
when DATA_TRUNCATED
|
303
|
+
raise Error, "unexpectedly data truncated"
|
304
|
+
when 1
|
305
|
+
raise Error, error_msg
|
306
|
+
else
|
307
|
+
@result_binds.map{|bind| bind.value}
|
308
|
+
end
|
309
|
+
end
|
310
|
+
|
311
|
+
# Iterates over all rows in this result set.
|
312
|
+
# @yield [Array] Called once for each row in this result set
|
313
|
+
# @see fetch_row
|
314
|
+
def each
|
315
|
+
while row = fetch
|
316
|
+
yield row
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
# Seeks to an arbitrary row in the result set.
|
321
|
+
# @param [offset] the row to seek to
|
322
|
+
# @return [self]
|
323
|
+
def data_seek( offset )
|
324
|
+
raise Error, "Statment already closed" unless @stmt
|
325
|
+
C::mysql_stmt_data_seek( @stmt, offset )
|
326
|
+
self
|
327
|
+
end
|
328
|
+
|
329
|
+
|
330
|
+
# @return [Integer] the number of rows changed by the latest
|
331
|
+
# execution of this statement
|
332
|
+
def affected_rows
|
333
|
+
raise Error, "Statment already closed" unless @stmt
|
334
|
+
C::mysql_stmt_affected_rows(@stmt)
|
335
|
+
end
|
336
|
+
|
337
|
+
# @return [Integer] the number of columns of this statement
|
338
|
+
def field_count
|
339
|
+
raise Error, "Statment already closed" unless @stmt
|
340
|
+
C::mysql_stmt_field_count(@stmt)
|
341
|
+
end
|
342
|
+
|
343
|
+
# Frees the current result of this statement.
|
344
|
+
# @return [self]
|
345
|
+
def free_result
|
346
|
+
raise Error, "Statment already closed" unless @stmt
|
347
|
+
if C::mysql_stmt_free_result(@stmt) != 0
|
348
|
+
raise Error, error_msg
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
# @return [Integer] the number of rows of this statement
|
353
|
+
def num_rows
|
354
|
+
raise Error, "Statment already closed" unless @stmt
|
355
|
+
C::mysql_stmt_num_rows(@stmt)
|
356
|
+
end
|
357
|
+
|
358
|
+
# @return [Integer] the number of parameters of this statement
|
359
|
+
def param_count
|
360
|
+
raise Error, "Statment already closed" unless @stmt
|
361
|
+
C::mysql_stmt_param_count(@stmt)
|
362
|
+
end
|
363
|
+
|
364
|
+
# @return [Integer] the number of the current row of the most recent result
|
365
|
+
def row_tell
|
366
|
+
raise Error, "Statment already closed" unless @stmt
|
367
|
+
C::mysql_stmt_row_tell(@stmt)
|
368
|
+
end
|
369
|
+
|
370
|
+
# Sets the position of the row cursor.
|
371
|
+
# @param [Integer] offset the new position of the row cursor
|
372
|
+
# @return [Integer] the former position of the row cursor
|
373
|
+
def row_seek( offset )
|
374
|
+
raise Error, "Statment already closed" unless @stmt
|
375
|
+
C::mysql_stmt_row_seek(@stmt, offset)
|
376
|
+
end
|
377
|
+
|
378
|
+
# @return [Result] the result metadata of the current statement
|
379
|
+
def result_metadata
|
380
|
+
raise Error, "Statment already closed" unless @stmt
|
381
|
+
result = C::mysql_stmt_result_metadata(@stmt)
|
382
|
+
if result.null?
|
383
|
+
if C::mysql_stmt_errno(@stmt) != 0
|
384
|
+
raise Error, error_msg
|
385
|
+
end
|
386
|
+
nil
|
387
|
+
else
|
388
|
+
Result.new(@mysql, result)
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
# @return [Integer] the value of an auto-increment column of
|
393
|
+
# the last INSERT or UPDATE statement
|
394
|
+
def insert_id
|
395
|
+
raise Error, "Statment already closed" unless @stmt
|
396
|
+
C::mysql_stmt_insert_id(@stmt)
|
397
|
+
end
|
398
|
+
|
399
|
+
# @return [String] the SQLSTATE error code for this statement
|
400
|
+
def sqlstate
|
401
|
+
raise Error, "Statment already closed" unless @stmt
|
402
|
+
C::mysql_stmt_sqlstate(@stmt)
|
403
|
+
end
|
404
|
+
|
405
|
+
private
|
406
|
+
def error_msg
|
407
|
+
C::mysql_stmt_error(@stmt)
|
408
|
+
end
|
409
|
+
|
410
|
+
def self.finalizer(stmt)
|
411
|
+
Proc.new do |*args|
|
412
|
+
C::mysql_stmt_close(stmt)
|
413
|
+
end
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
data/lib/mysql/time.rb
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
class Mysql
|
2
|
+
class Time
|
3
|
+
def initialize(year=0, month=0, day=0, hour=0, minute=0, second=0, neg=false, second_part=0)
|
4
|
+
@year, @month, @day, @hour, @minute, @second, @neg, @second_part =
|
5
|
+
year.to_i, month.to_i, day.to_i, hour.to_i, minute.to_i, second.to_i, neg, second_part.to_i
|
6
|
+
end
|
7
|
+
attr_accessor :year, :month, :day, :hour, :minute, :second, :neg, :second_part
|
8
|
+
alias mon month
|
9
|
+
alias min minute
|
10
|
+
alias sec second
|
11
|
+
|
12
|
+
def ==(other)
|
13
|
+
other.is_a?(Mysql::Time) &&
|
14
|
+
@year == other.year && @month == other.month && @day == other.day &&
|
15
|
+
@hour == other.hour && @minute == other.minute && @second == other.second &&
|
16
|
+
@neg == neg && @second_part == other.second_part
|
17
|
+
end
|
18
|
+
|
19
|
+
def eql?(other)
|
20
|
+
self == other
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_s
|
24
|
+
if year == 0 and mon == 0 and day == 0
|
25
|
+
h = neg ? hour * -1 : hour
|
26
|
+
sprintf "%02d:%02d:%02d", h, min, sec
|
27
|
+
else
|
28
|
+
sprintf "%04d-%02d-%02d %02d:%02d:%02d", year, mon, day, hour, min, sec
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
end
|