ffi-mysql 0.0.1

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