ffi-mysql 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,365 @@
1
+ require 'ffi'
2
+
3
+ # @author Frank Fischer
4
+ #
5
+ # Basic MySQL class, provides interface to a server.
6
+ class Mysql
7
+
8
+ # FFI interface.
9
+ module C
10
+ extend FFI::Library
11
+ #ffi_lib ["mysqlclient", "libmysqlclient.so.15"]
12
+ ffi_lib ["mysqlclient", "libmysqlclient.so.15", "libmysqlclient.so.16"]
13
+
14
+ # FieldType = enum(:decimal, Mysql::Field::TYPE_DECIMAL,
15
+ # :tiny, Mysql::Field::TYPE_TINY,
16
+ # :short, Mysql::Field::TYPE_SHORT,
17
+ # :long, Mysql::Field::TYPE_LONG,
18
+ # :float, Mysql::Field::TYPE_FLOAT,
19
+ # :double, Mysql::Field::TYPE_DOUBLE,
20
+ # :null, Mysql::Field::TYPE_NULL,
21
+ # :timestamp, Mysql::Field::TYPE_TIMESTAMP,
22
+ # :longlong, Mysql::Field::TYPE_LONGLONG,
23
+ # :int24, Mysql::Field::TYPE_INT24,
24
+ # :date, Mysql::Field::TYPE_DATE,
25
+ # :time, Mysql::Field::TYPE_TIME,
26
+ # :datetime, Mysql::Field::TYPE_DATETIME,
27
+ # :year, Mysql::Field::TYPE_YEAR,
28
+ # :newdate, Mysql::Field::TYPE_NEWDATE,
29
+ # :varchar, Mysql::Field::TYPE_VARCHAR,
30
+ # :bit, Mysql::Field::TYPE_BIT,
31
+ # :newdecimal, Mysql::Field::TYPE_NEWDECIMAL,
32
+ # :enum, Mysql::Field::TYPE_ENUM,
33
+ # :set, Mysql::Field::TYPE_SET,
34
+ # :tiny_blob, Mysql::Field::TYPE_TINY_BLOB,
35
+ # :medium_blob, Mysql::Field::TYPE_MEDIUM_BLOB,
36
+ # :long_blob, Mysql::Field::TYPE_LONG_BLOB,
37
+ # :blob, Mysql::Field::TYPE_BLOB,
38
+ # :var_string, Mysql::Field::TYPE_VAR_STRING,
39
+ # :string, Mysql::Field::TYPE_STRING,
40
+ # :geometry, Mysql::Field::TYPE_GEOMETRY,
41
+ # :char, Mysql::Field::TYPE_CHAR,
42
+ # :interval, Mysql::Field::TYPE_INTERVAL)
43
+ FieldType = :uchar
44
+
45
+ class Field < FFI::Struct
46
+ layout(:name, :string,
47
+ :org_name, :string,
48
+ :table, :string,
49
+ :org_table, :string,
50
+ :db, :string,
51
+ :catalog, :string,
52
+ :def, :string,
53
+ :length, :ulong,
54
+ :max_length, :ulong,
55
+ :name_length, :uint,
56
+ :org_name_length, :uint,
57
+ :table_length, :uint,
58
+ :org_table_length, :uint,
59
+ :db_length, :uint,
60
+ :catalog_length, :uint,
61
+ :def_length, :uint,
62
+ :flags, :uint,
63
+ :decimals, :uint,
64
+ :charsetnr, :uint,
65
+ :type, FieldType)
66
+ end
67
+
68
+ StmtAttrType = enum( :update_max_length, :cursor_type, :prefetch_rows )
69
+
70
+ attach_function :mysql_init, [:pointer], :pointer
71
+ attach_function :mysql_close, [:pointer], :void
72
+ attach_function :mysql_error, [:pointer], :string
73
+ attach_function :mysql_get_client_version, [], :int
74
+ attach_function :mysql_get_client_info, [], :string
75
+ attach_function :mysql_get_server_version, [:pointer], :int
76
+ attach_function :mysql_get_server_info, [:pointer], :string
77
+ attach_function :mysql_real_connect, [:pointer, :string, :string, :string, :string, :uint, :string, :ulong], :pointer
78
+ attach_function :mysql_options, [:pointer, :int, :pointer], :int
79
+ attach_function :mysql_set_server_option, [:pointer, :int], :int
80
+ attach_function :mysql_real_query, [:pointer, :string, :ulong], :int
81
+ attach_function :mysql_field_count, [:pointer], :uint
82
+ attach_function :mysql_store_result, [:pointer], :pointer
83
+ attach_function :mysql_free_result, [:pointer], :void
84
+ attach_function :mysql_next_result, [:pointer], :int
85
+ attach_function :mysql_more_results, [:pointer], :bool
86
+ attach_function :mysql_affected_rows, [:pointer], :ulong_long
87
+ attach_function :mysql_num_rows, [:pointer], :ulong_long
88
+ attach_function :mysql_fetch_row, [:pointer], :pointer
89
+ attach_function :mysql_fetch_lengths, [:pointer], :pointer
90
+ attach_function :mysql_row_tell, [:pointer], :ulong_long
91
+ attach_function :mysql_row_seek, [:pointer, :ulong_long], :ulong_long
92
+ attach_function :mysql_num_fields, [:pointer], :uint
93
+ attach_function :mysql_fetch_field, [:pointer], :pointer
94
+ attach_function :mysql_fetch_field_direct, [:pointer, :uint], :pointer
95
+ attach_function :mysql_field_tell, [:pointer], :uint
96
+ attach_function :mysql_field_seek, [:pointer, :uint], :uint
97
+ attach_function :mysql_data_seek, [:pointer, :ulong_long], :void
98
+ attach_function :mysql_sqlstate, [:pointer], :string
99
+ attach_function :mysql_stmt_init, [:pointer], :pointer
100
+ attach_function :mysql_stmt_attr_set, [:pointer, StmtAttrType, :pointer], :int
101
+ attach_function :mysql_stmt_close, [:pointer], :void
102
+ attach_function :mysql_stmt_prepare, [:pointer, :string, :ulong], :int
103
+ attach_function :mysql_stmt_execute, [:pointer], :int
104
+ attach_function :mysql_stmt_free_result, [:pointer], :int
105
+ attach_function :mysql_stmt_param_count, [:pointer], :ulong
106
+ attach_function :mysql_stmt_bind_param, [:pointer, :pointer], :char
107
+ attach_function :mysql_stmt_result_metadata, [:pointer], :pointer
108
+ attach_function :mysql_stmt_bind_result, [:pointer, :pointer], :char
109
+ attach_function :mysql_stmt_affected_rows, [:pointer], :ulong_long
110
+ attach_function :mysql_stmt_store_result, [:pointer], :int
111
+ attach_function :mysql_stmt_fetch, [:pointer], :int
112
+ attach_function :mysql_stmt_data_seek, [:pointer, :ulong_long], :void
113
+ attach_function :mysql_stmt_field_count, [:pointer], :uint
114
+ attach_function :mysql_stmt_num_rows, [:pointer], :ulong_long
115
+ attach_function :mysql_stmt_row_tell, [:pointer], :ulong_long
116
+ attach_function :mysql_stmt_row_seek, [:pointer, :ulong_long], :ulong_long
117
+ attach_function :mysql_stmt_insert_id, [:pointer], :ulong_long
118
+ attach_function :mysql_stmt_sqlstate, [:pointer], :string
119
+ attach_function :mysql_stmt_errno, [:pointer], :uint
120
+ attach_function :mysql_stmt_error, [:pointer], :string
121
+ end
122
+
123
+ # Creates a new MySQL object.
124
+ def self.init
125
+ mysql = allocate
126
+ mysql.send :initialize
127
+ mysql
128
+ end
129
+
130
+ # Creates a new MySQL connector and opens a connection.
131
+ def self.new( host = nil, user = nil, passwd = nil, db = nil, port = 0, sock = nil, flag = 0)
132
+ mysql = allocate
133
+ mysql.send :initialize
134
+ mysql.real_connect( host, user, passwd, db, port, sock, flag )
135
+ mysql
136
+ end
137
+
138
+ # @return [Integer] the version of the client
139
+ def self.client_version
140
+ C::mysql_get_client_version
141
+ end
142
+
143
+ # @return [Integer] the version of the client
144
+ def client_version
145
+ Mysql.client_version
146
+ end
147
+
148
+ # @return [String] string containing the client's version
149
+ def self.client_info
150
+ C::mysql_get_client_info
151
+ end
152
+
153
+ # Escape special character in MySQL.
154
+ # === Note
155
+ # In Ruby 1.8, this is not safe for multibyte charset such as 'SJIS'.
156
+ # You should use place-holder in prepared-statement.
157
+ def self.escape_string(str)
158
+ str.gsub(/[\0\n\r\\\'\"\x1a]/) do |s|
159
+ case s
160
+ when "\0" then "\\0"
161
+ when "\n" then "\\n"
162
+ when "\r" then "\\r"
163
+ when "\x1a" then "\\Z"
164
+ else "\\#{s}"
165
+ end
166
+ end
167
+ end
168
+
169
+ class << self
170
+ alias :real_connect :new
171
+ alias :connect :new
172
+ alias :get_client_version :client_version
173
+ alias :get_client_info :client_info
174
+ alias quote escape_string
175
+ end
176
+
177
+
178
+ # if true (the default), query return the first result
179
+ attr_accessor :query_with_result
180
+
181
+ # Creates a new connection to a MySQL server.
182
+ #
183
+ # @param (see Mysql#real_connect)
184
+ def initialize
185
+ @mysql_free = [true]
186
+ @mysql = C::mysql_init( nil )
187
+ @connected = false
188
+ @query_with_result = true
189
+ end
190
+
191
+ # internal finalizer
192
+ def self.finalizer(mysql, mysql_free)
193
+ Proc.new do |*args|
194
+ unless mysql_free[0]
195
+ C::mysql_close(mysql)
196
+ end
197
+ end
198
+ end
199
+
200
+ # Opens a new connection to a MySQL server.
201
+ #
202
+ # @param [String] host the MySQL server
203
+ # @param [String] user the username to login
204
+ # @param [String] passwd the user's password
205
+ # @param [String] db the name of the database to use
206
+ # @param [Integer] port the port of the server to use
207
+ # @param [Integer] flag connection flags
208
+ def real_connect( host = nil, user = nil, passwd = nil, db = nil, port = 0, sock = nil, flag = 0)
209
+ raise Error, "Already connected to a MySQL server" if @connected
210
+
211
+ ObjectSpace.define_finalizer( self, Mysql.finalizer(@mysql, @mysql_free))
212
+
213
+ if C::mysql_real_connect( @mysql, host, user, passwd, db, port, sock, flag ).null?
214
+ raise Mysql::Error, error_msg
215
+ end
216
+
217
+ @connected = true
218
+ self
219
+ end
220
+ alias :connect :real_connect
221
+
222
+ # Closes the connection to the server.
223
+ def close
224
+ C::mysql_close(@mysql)
225
+ @mysql = nil
226
+ @mysql_free[0] = true
227
+ @connected = false
228
+ end
229
+
230
+ # @return [Integer] the version of the server
231
+ def server_version
232
+ C::mysql_get_server_version(@mysql)
233
+ end
234
+ alias get_server_version server_version
235
+
236
+ # @return [String] string containing the server's version
237
+ def server_info
238
+ C::mysql_get_server_info(@mysql)
239
+ end
240
+ alias get_server_info server_info
241
+
242
+ # @return [String] the SQLSTATE error code for the most recent statement
243
+ def sqlstate
244
+ C::mysql_sqlstate(@mysql)
245
+ end
246
+
247
+
248
+ # Sets extra connection options.
249
+ #
250
+ # @param [Integer] option the option to set
251
+ # @param [String,Integer,true,false,nil] the value of the option to set
252
+ def options( arg, value = nil )
253
+ result = if value.nil?
254
+ C::mysql_options( @mysql, arg, nil )
255
+ elsif value.kind_of? Integer
256
+ C::mysql_options( @mysql, arg, FFI::MemoryPointer.new(:uint).write_int(value) )
257
+ elsif value.kind_of? String
258
+ C::mysql_options( @mysql, arg, FFI::MemoryPointer.from_string(value) )
259
+ elsif value == true
260
+ C::mysql_options( @mysql, arg, FFI::MemoryPointer.new(:uint).write_int(1) )
261
+ elsif value == false
262
+ C::mysql_options( @mysql, arg, FFI::MemoryPointer.new(:uint).write_int(0) )
263
+ else
264
+ raise ArgumentError, "value must one of [String, Integer, nil, true, false]"
265
+ end
266
+ raise Error, error_msg if result != 0
267
+ self
268
+ end
269
+
270
+ # Enables or disables an options for the connection.
271
+ #
272
+ # @param [Integer] option server option
273
+ def set_server_option( option )
274
+ if C::mysql_set_server_option( @mysql, option ) != 0
275
+ raise Error, error_msg
276
+ end
277
+ self
278
+ end
279
+
280
+
281
+ # Execute a query statement.
282
+ #
283
+ # @param [String] sql the SQL statement
284
+ # @yield [optional, Result] calls block once per result set
285
+ #
286
+ # @return [Result,self] the first result set if no block is given, self otherwise
287
+ def query(sql)
288
+ raise Error, "Not connected" unless @connected
289
+ if C::mysql_real_query(@mysql, sql, sql.size) != 0
290
+ raise Error, error_msg
291
+ end
292
+
293
+ if block_given?
294
+ begin
295
+ result = store_result
296
+ yield result
297
+ ensure
298
+ result.free
299
+ end while next_result
300
+ self
301
+ elsif query_with_result
302
+ if field_count == 0
303
+ nil
304
+ else
305
+ store_result
306
+ end
307
+ else
308
+ self
309
+ end
310
+ end
311
+
312
+ # Stores the current result in a result set.
313
+ # @return [Result] the result set
314
+ def store_result
315
+ Result.new(@mysql, C::mysql_store_result(@mysql))
316
+ end
317
+
318
+ # Advances to the next result set.
319
+ # @return [true,false] true if there's another result set
320
+ def next_result
321
+ result = C::mysql_next_result(@mysql)
322
+ if result == 0
323
+ true
324
+ elsif result < 0
325
+ false
326
+ else
327
+ raise Error, error_msg
328
+ end
329
+ end
330
+
331
+ # @return [true,false] true if there's another result set
332
+ def more_results?
333
+ C::mysql_more_results(@mysql)
334
+ end
335
+ alias more_results more_results?
336
+
337
+ # @return [Integer] the number of affected rows by the last query
338
+ def affected_rows
339
+ C::mysql_affected_rows(@mysql)
340
+ end
341
+
342
+ # @return [Integer] the number of columns for the most recent query
343
+ def field_count
344
+ C::mysql_field_count(@mysql)
345
+ end
346
+
347
+ # @return [Stmt] a new statement
348
+ def stmt_init
349
+ Stmt.new( @mysql )
350
+ end
351
+
352
+ # Creates and prepares a new statement.
353
+ # @param [String] stmt the SQL statement
354
+ # @return [Stmt] the new prepared statement
355
+ def prepare( stmt )
356
+ stmt_init.prepare(stmt)
357
+ end
358
+
359
+ # Returns the current error message.
360
+ def error_msg
361
+ C::mysql_error(@mysql)
362
+ end
363
+ private :error_msg
364
+
365
+ end
@@ -0,0 +1,168 @@
1
+ class Mysql
2
+
3
+ # Result set.
4
+ class Result
5
+ include Enumerable
6
+
7
+ attr_reader :fields
8
+
9
+ # Create the next result object.
10
+ def initialize( mysql, result )
11
+ @mysql = mysql
12
+ @result = result
13
+ @num_rows = @num_fields = nil
14
+ raise ArgumentError, "Invalid result object" if @result.nil? or @result.null?
15
+ ObjectSpace.define_finalizer( self, Result.finalizer(@result) )
16
+ end
17
+
18
+ # Frees the result object.
19
+ def free
20
+ C::mysql_free_result(@result)
21
+ @result = nil
22
+ ObjectSpace.undefine_finalizer( self )
23
+ end
24
+
25
+ # @return [Integer] the number of rows in this result set
26
+ def num_rows
27
+ raise Error, "Result has been freed" unless @result
28
+ @num_rows ||= C::mysql_num_rows(@result)
29
+ end
30
+
31
+ # @return [Integer] the number of columns in this result set
32
+ def num_fields
33
+ raise Error, "Result has been freed" unless @result
34
+ @num_fields ||= C::mysql_num_fields(@result)
35
+ end
36
+
37
+ # @return [Array<Integer>] the array of number of chars for each column
38
+ def fetch_lengths
39
+ raise Error, "Result has been freed" unless @result
40
+ lengths = C::mysql_fetch_lengths(@result)
41
+ lengths.null? ? nil : lengths.read_array_of_long(num_fields)
42
+ end
43
+
44
+ # @return [Array<String>] Ary of elements of the next row.
45
+ def fetch_row
46
+ raise Error, "Result has been freed" unless @result
47
+ row = C::mysql_fetch_row(@result)
48
+ if row.null?
49
+ nil
50
+ else
51
+ lengths = fetch_lengths
52
+ row = row.read_array_of_pointer(lengths.size)
53
+ (0...lengths.size).map{|i|
54
+ row[i].null? ? nil : row[i].read_string(lengths[i])
55
+ }
56
+ end
57
+ end
58
+
59
+ # Iterates over all rows in this result set.
60
+ # @yield [Array<String>] Called once for each row in this result set
61
+ # @see fetch_row
62
+ def each
63
+ while row = fetch_row
64
+ yield row
65
+ end
66
+ end
67
+
68
+ # @return [Integer] the current position of the row cursor
69
+ def row_tell
70
+ raise Error, "Result has been freed" unless @result
71
+ C::mysql_row_tell(@result)
72
+ end
73
+
74
+ # Sets the position of the row cursor.
75
+ # @param [Integer] offset the new position of the row cursor
76
+ # @return [Integer] the former position of the row cursor
77
+ def row_seek( offset )
78
+ raise Error, "Result has been freed" unless @result
79
+ C::mysql_row_seek(@result, offset)
80
+ end
81
+
82
+ # @param [Boolean] with_table if true, fields are denoted with table "tablename.fieldname"
83
+ # @return [Hash] hash of elements "field" => "value"
84
+ def fetch_hash(with_table = false)
85
+ return nil unless row = fetch_row
86
+ keys = if with_table
87
+ @tblcolnames ||= fetch_fields.map{|f| "#{f.table}.#{f.name}"}
88
+ else
89
+ @colnames ||= fetch_fields.map{|f| f.name}
90
+ end
91
+
92
+ hash = {}
93
+ row.each_with_index do |value, i|
94
+ hash[keys[i]] = value
95
+ end
96
+ hash
97
+ end
98
+
99
+ # Iterates over all rows in this result set.
100
+ # @param [Boolean] with_table if true, fields are denoted with table "tablename.fieldname"
101
+ # @yield [Hash] Called once for each row in this result set with a row-hash
102
+ # @see fetch_hash
103
+ def each_hash(with_table = false)
104
+ while row = fetch_hash(with_table)
105
+ yield row
106
+ end
107
+ end
108
+
109
+ # @return [Integer] The position of the field cursor after the last fetch_field.
110
+ def field_tell
111
+ raise Error, "Result has been freed" unless @result
112
+ C::mysql_field_tell(@result)
113
+ end
114
+
115
+ # Sets the field cursor to the given offset.
116
+ # @param [Integer] the new field offset
117
+ # @return [Integer] the position of the previous field cursor
118
+ def field_seek(offset)
119
+ raise Error, "Result has been freed" unless @result
120
+ C::mysql_field_seek(@result, offset)
121
+ end
122
+
123
+ # @return [Field,nil] the information for the next Field or nil
124
+ def fetch_field
125
+ raise Error, "Result has been freed" unless @result
126
+ field_ptr = C::mysql_fetch_field(@result)
127
+ if field_ptr.null?
128
+ nil
129
+ else
130
+ Field.new(C::Field.new(field_ptr))
131
+ end
132
+ end
133
+
134
+ # @param [Integer] fieldnr column number
135
+ # @return [Field] the information for field of column fieldnr
136
+ def fetch_field_direct( fieldnr )
137
+ raise Error, "Result has been freed" unless @result
138
+ n = num_fields
139
+ raise Error, "#{fieldnr}: out of range (max: #{n})" if fieldnr < 0 or fieldnr >= n
140
+ field_ptr = C::mysql_fetch_field_direct(@result, fieldnr)
141
+ Field.new(C::Field.new(field_ptr))
142
+ end
143
+
144
+ # @return [Array<Field>] array of field-informations for each column in this result set
145
+ def fetch_fields
146
+ raise Error, "Result has been freed" unless @result
147
+ n = num_fields
148
+ (0...n).map{|i| fetch_field_direct(i)}
149
+ end
150
+
151
+ # Seeks to an arbitrary row in the result set.
152
+ # @param [Integer] row the number of row to use next
153
+ # @return [self]
154
+ def data_seek( row )
155
+ raise Error, "Result has been freed" unless @result
156
+ C::mysql_data_seek(@result, row)
157
+ self
158
+ end
159
+
160
+ # Internal finalizer, calls self.free.
161
+ def self.finalizer(result)
162
+ Proc.new do |*args|
163
+ C::mysql_free_result(result)
164
+ end
165
+ end
166
+ end
167
+
168
+ end