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.
@@ -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