activerecord-ruby_mysql-adapter 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,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: