activerecord-ruby_mysql-adapter 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,425 @@
1
+ require 'active_record/connection_adapters/abstract_mysql_adapter'
2
+ require 'active_record/connection_adapters/statement_pool'
3
+ require 'active_support/core_ext/hash/keys'
4
+
5
+ require 'mysql'
6
+
7
+ class Mysql
8
+ class Time
9
+ ###
10
+ # This monkey patch is for test_additional_columns_from_join_table
11
+ def to_date
12
+ Date.new(year, month, day)
13
+ end
14
+ end
15
+ class Stmt; include Enumerable end
16
+ class Result; include Enumerable end
17
+ end
18
+
19
+ module ActiveRecord
20
+ class Base
21
+ # Establishes a connection to the database that's used by all Active Record objects.
22
+ def self.ruby_mysql_connection(config) # :nodoc:
23
+ config = config.symbolize_keys
24
+ host = config[:host]
25
+ port = config[:port]
26
+ socket = config[:socket]
27
+ username = config[:username] ? config[:username].to_s : 'root'
28
+ password = config[:password].to_s
29
+ database = config[:database]
30
+
31
+ mysql = Mysql.init
32
+ mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey]
33
+
34
+ default_flags = Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? Mysql::CLIENT_MULTI_RESULTS : 0
35
+ default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS)
36
+ options = [host, username, password, database, port, socket, default_flags]
37
+ ConnectionAdapters::RubyMysqlAdapter.new(mysql, logger, options, config)
38
+ end
39
+ end
40
+
41
+ module ConnectionAdapters
42
+ # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with
43
+ # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/).
44
+ #
45
+ # Options:
46
+ #
47
+ # * <tt>:host</tt> - Defaults to "localhost".
48
+ # * <tt>:port</tt> - Defaults to 3306.
49
+ # * <tt>:socket</tt> - Defaults to "/tmp/mysql.sock".
50
+ # * <tt>:username</tt> - Defaults to "root"
51
+ # * <tt>:password</tt> - Defaults to nothing.
52
+ # * <tt>:database</tt> - The name of the database. No default, must be provided.
53
+ # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection.
54
+ # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html).
55
+ # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection.
56
+ # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection.
57
+ # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection.
58
+ # * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection.
59
+ # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection.
60
+ #
61
+ class RubyMysqlAdapter < AbstractMysqlAdapter
62
+
63
+ class Column < AbstractMysqlAdapter::Column #:nodoc:
64
+ def self.string_to_time(value)
65
+ return super unless Mysql::Time === value
66
+ new_time(
67
+ value.year,
68
+ value.month,
69
+ value.day,
70
+ value.hour,
71
+ value.minute,
72
+ value.second,
73
+ value.second_part)
74
+ end
75
+
76
+ def self.string_to_dummy_time(v)
77
+ return super unless Mysql::Time === v
78
+ new_time(2000, 01, 01, v.hour, v.minute, v.second, v.second_part)
79
+ end
80
+
81
+ def self.string_to_date(v)
82
+ return super unless Mysql::Time === v
83
+ new_date(v.year, v.month, v.day)
84
+ end
85
+
86
+ def adapter
87
+ RubyMysqlAdapter
88
+ end
89
+ end
90
+
91
+ ADAPTER_NAME = 'RubyMysql'
92
+
93
+ class StatementPool < ConnectionAdapters::StatementPool
94
+ def initialize(connection, max = 1000)
95
+ super
96
+ @cache = Hash.new { |h,pid| h[pid] = {} }
97
+ end
98
+
99
+ def each(&block); cache.each(&block); end
100
+ def key?(key); cache.key?(key); end
101
+ def [](key); cache[key]; end
102
+ def length; cache.length; end
103
+ def delete(key); cache.delete(key); end
104
+
105
+ def []=(sql, key)
106
+ while @max <= cache.size
107
+ cache.shift.last[:stmt].close
108
+ end
109
+ cache[sql] = key
110
+ end
111
+
112
+ def clear
113
+ cache.values.each do |hash|
114
+ hash[:stmt].close
115
+ end
116
+ cache.clear
117
+ end
118
+
119
+ private
120
+ def cache
121
+ @cache[$$]
122
+ end
123
+ end
124
+
125
+ def initialize(connection, logger, connection_options, config)
126
+ super
127
+ @statements = StatementPool.new(@connection,
128
+ config.fetch(:statement_limit) { 1000 })
129
+ @client_encoding = nil
130
+ connect
131
+ end
132
+
133
+ # Returns true, since this connection adapter supports prepared statement
134
+ # caching.
135
+ def supports_statement_cache?
136
+ true
137
+ end
138
+
139
+ # HELPER METHODS ===========================================
140
+
141
+ def each_hash(result) # :nodoc:
142
+ if block_given?
143
+ result.each_hash do |row|
144
+ row.symbolize_keys!
145
+ yield row
146
+ end
147
+ else
148
+ to_enum(:each_hash, result)
149
+ end
150
+ end
151
+
152
+ def new_column(field, default, type, null, collation) # :nodoc:
153
+ Column.new(field, default, type, null, collation)
154
+ end
155
+
156
+ def error_number(exception) # :nodoc:
157
+ exception.errno if exception.respond_to?(:errno)
158
+ end
159
+
160
+ # QUOTING ==================================================
161
+
162
+ def type_cast(value, column)
163
+ return super unless value == true || value == false
164
+
165
+ value ? 1 : 0
166
+ end
167
+
168
+ def quote_string(string) #:nodoc:
169
+ @connection.quote(string)
170
+ end
171
+
172
+ # CONNECTION MANAGEMENT ====================================
173
+
174
+ def active?
175
+ if @connection.respond_to?(:stat)
176
+ @connection.stat
177
+ else
178
+ @connection.query 'select 1'
179
+ end
180
+
181
+ # mysql-ruby doesn't raise an exception when stat fails.
182
+ if @connection.respond_to?(:errno)
183
+ @connection.errno.zero?
184
+ else
185
+ true
186
+ end
187
+ rescue Mysql::Error
188
+ false
189
+ end
190
+
191
+ def reconnect!
192
+ disconnect!
193
+ clear_cache!
194
+ connect
195
+ end
196
+
197
+ # Disconnects from the database if already connected. Otherwise, this
198
+ # method does nothing.
199
+ def disconnect!
200
+ @connection.close rescue nil
201
+ end
202
+
203
+ def reset!
204
+ if @connection.respond_to?(:change_user)
205
+ # See http://bugs.mysql.com/bug.php?id=33540 -- the workaround way to
206
+ # reset the connection is to change the user to the same user.
207
+ @connection.change_user(@config[:username], @config[:password], @config[:database])
208
+ configure_connection
209
+ end
210
+ end
211
+
212
+ # DATABASE STATEMENTS ======================================
213
+
214
+ def select_rows(sql, name = nil)
215
+ @connection.query_with_result = true
216
+ rows = exec_without_stmt(sql, name).rows
217
+ @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
218
+ rows
219
+ end
220
+
221
+ # Clears the prepared statements cache.
222
+ def clear_cache!
223
+ @statements.clear
224
+ end
225
+
226
+ if "<3".respond_to?(:encode)
227
+ # Taken from here:
228
+ # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb
229
+ # Author: TOMITA Masahiro <tommy@tmtm.org>
230
+ ENCODINGS = {
231
+ "armscii8" => nil,
232
+ "ascii" => Encoding::US_ASCII,
233
+ "big5" => Encoding::Big5,
234
+ "binary" => Encoding::ASCII_8BIT,
235
+ "cp1250" => Encoding::Windows_1250,
236
+ "cp1251" => Encoding::Windows_1251,
237
+ "cp1256" => Encoding::Windows_1256,
238
+ "cp1257" => Encoding::Windows_1257,
239
+ "cp850" => Encoding::CP850,
240
+ "cp852" => Encoding::CP852,
241
+ "cp866" => Encoding::IBM866,
242
+ "cp932" => Encoding::Windows_31J,
243
+ "dec8" => nil,
244
+ "eucjpms" => Encoding::EucJP_ms,
245
+ "euckr" => Encoding::EUC_KR,
246
+ "gb2312" => Encoding::EUC_CN,
247
+ "gbk" => Encoding::GBK,
248
+ "geostd8" => nil,
249
+ "greek" => Encoding::ISO_8859_7,
250
+ "hebrew" => Encoding::ISO_8859_8,
251
+ "hp8" => nil,
252
+ "keybcs2" => nil,
253
+ "koi8r" => Encoding::KOI8_R,
254
+ "koi8u" => Encoding::KOI8_U,
255
+ "latin1" => Encoding::ISO_8859_1,
256
+ "latin2" => Encoding::ISO_8859_2,
257
+ "latin5" => Encoding::ISO_8859_9,
258
+ "latin7" => Encoding::ISO_8859_13,
259
+ "macce" => Encoding::MacCentEuro,
260
+ "macroman" => Encoding::MacRoman,
261
+ "sjis" => Encoding::SHIFT_JIS,
262
+ "swe7" => nil,
263
+ "tis620" => Encoding::TIS_620,
264
+ "ucs2" => Encoding::UTF_16BE,
265
+ "ujis" => Encoding::EucJP_ms,
266
+ "utf8" => Encoding::UTF_8,
267
+ "utf8mb4" => Encoding::UTF_8,
268
+ }
269
+ else
270
+ ENCODINGS = Hash.new { |h,k| h[k] = k }
271
+ end
272
+
273
+ # Get the client encoding for this database
274
+ def client_encoding
275
+ return @client_encoding if @client_encoding
276
+
277
+ result = exec_query(
278
+ "SHOW VARIABLES WHERE Variable_name = 'character_set_client'",
279
+ 'SCHEMA')
280
+ @client_encoding = ENCODINGS[result.rows.last.last]
281
+ end
282
+
283
+ def exec_query(sql, name = 'SQL', binds = [])
284
+ log(sql, name, binds) do
285
+ exec_stmt(sql, name, binds) do |cols, stmt|
286
+ ActiveRecord::Result.new(cols, stmt.to_a) if cols
287
+ end
288
+ end
289
+ end
290
+
291
+ def last_inserted_id(result)
292
+ @connection.insert_id
293
+ end
294
+
295
+ def exec_without_stmt(sql, name = 'SQL') # :nodoc:
296
+ # Some queries, like SHOW CREATE TABLE don't work through the prepared
297
+ # statement API. For those queries, we need to use this method. :'(
298
+ log(sql, name) do
299
+ result = @connection.query(sql)
300
+ cols = []
301
+ rows = []
302
+
303
+ if result
304
+ cols = result.fetch_fields.map { |field| field.name }
305
+ rows = result.to_a
306
+ result.free
307
+ end
308
+ ActiveRecord::Result.new(cols, rows)
309
+ end
310
+ end
311
+
312
+ def execute_and_free(sql, name = nil)
313
+ result = execute(sql, name)
314
+ ret = yield result
315
+ result.free
316
+ ret
317
+ end
318
+
319
+ def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
320
+ super sql, name
321
+ id_value || @connection.insert_id
322
+ end
323
+ alias :create :insert_sql
324
+
325
+ def exec_delete(sql, name, binds)
326
+ log(sql, name, binds) do
327
+ exec_stmt(sql, name, binds) do |cols, stmt|
328
+ stmt.affected_rows
329
+ end
330
+ end
331
+ end
332
+ alias :exec_update :exec_delete
333
+
334
+ def begin_db_transaction #:nodoc:
335
+ exec_without_stmt "BEGIN"
336
+ rescue Mysql::Error
337
+ # Transactions aren't supported
338
+ end
339
+
340
+ private
341
+
342
+ def exec_stmt(sql, name, binds)
343
+ cache = {}
344
+ if binds.empty?
345
+ stmt = @connection.prepare(sql)
346
+ else
347
+ cache = @statements[sql] ||= {
348
+ :stmt => @connection.prepare(sql)
349
+ }
350
+ stmt = cache[:stmt]
351
+ end
352
+
353
+ begin
354
+ stmt.execute(*binds.map { |col, val| type_cast(val, col) })
355
+ rescue Mysql::Error => e
356
+ # Older versions of MySQL leave the prepared statement in a bad
357
+ # place when an error occurs. To support older mysql versions, we
358
+ # need to close the statement and delete the statement from the
359
+ # cache.
360
+ stmt.close
361
+ @statements.delete sql
362
+ raise e
363
+ end
364
+
365
+ cols = nil
366
+ if metadata = stmt.result_metadata
367
+ cols = cache[:cols] ||= metadata.fetch_fields.map { |field|
368
+ field.name
369
+ }
370
+ end
371
+
372
+ result = yield [cols, stmt]
373
+
374
+ stmt.result_metadata.free if cols
375
+ stmt.free_result
376
+ stmt.close if binds.empty?
377
+
378
+ result
379
+ end
380
+
381
+ def connect
382
+ encoding = @config[:encoding]
383
+ if encoding
384
+ @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
385
+ end
386
+
387
+ if @config[:sslca] || @config[:sslkey]
388
+ @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher])
389
+ end
390
+
391
+ @connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout]
392
+ @connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout]
393
+ @connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout]
394
+
395
+ @connection.real_connect(*@connection_options)
396
+
397
+ # reconnect must be set after real_connect is called, because real_connect sets it to false internally
398
+ @connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=)
399
+
400
+ configure_connection
401
+ end
402
+
403
+ def configure_connection
404
+ encoding = @config[:encoding]
405
+ execute("SET NAMES '#{encoding}'", :skip_logging) if encoding
406
+
407
+ # By default, MySQL 'where id is null' selects the last inserted id.
408
+ # Turn this off. http://dev.rubyonrails.org/ticket/6778
409
+ execute("SET SQL_AUTO_IS_NULL=0", :skip_logging)
410
+ end
411
+
412
+ def select(sql, name = nil, binds = [])
413
+ @connection.query_with_result = true
414
+ rows = exec_query(sql, name, binds).to_a
415
+ @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped
416
+ rows
417
+ end
418
+
419
+ # Returns the version of the connected MySQL server.
420
+ def version
421
+ @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
422
+ end
423
+ end
424
+ end
425
+ end
metadata ADDED
@@ -0,0 +1,47 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activerecord-ruby_mysql-adapter
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.1'
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - tommy@tmtm.org
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-09-28 00:00:00.000000000 Z
13
+ dependencies: []
14
+ description: This is an ActiveRecord adapter for Ruby/MySQL. This is based on MySQL
15
+ adapter
16
+ email:
17
+ executables: []
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - lib/active_record/connection_adapters/ruby_mysql_adapter.rb
22
+ homepage:
23
+ licenses: []
24
+ post_install_message:
25
+ rdoc_options: []
26
+ require_paths:
27
+ - lib
28
+ required_ruby_version: !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ required_rubygems_version: !ruby/object:Gem::Requirement
35
+ none: false
36
+ requirements:
37
+ - - ! '>='
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ requirements: []
41
+ rubyforge_project:
42
+ rubygems_version: 1.8.23
43
+ signing_key:
44
+ specification_version: 3
45
+ summary: An ActiveRecord adapter for Ruby/MySQL
46
+ test_files: []
47
+ has_rdoc: