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