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/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