activerecord-mysql2-adapter 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +17 -0
- data/.rvmrc +1 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/activerecord-mysql2-adapter.gemspec +18 -0
- data/lib/active_record/connection_adapters/em_mysql2_adapter.rb +64 -0
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +619 -0
- data/lib/active_record/fiber_patches.rb +132 -0
- data/lib/activerecord-mysql2-adapter.rb +8 -0
- data/lib/activerecord-mysql2-adapter/version.rb +7 -0
- data/lib/arel/engines/sql/compilers/mysql2_compiler.rb +11 -0
- metadata +74 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use 1.9.3@mysql2 --create
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Matthias Viehweger
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Activerecord::Mysql2::Adapter
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'activerecord-mysql2-adapter'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install activerecord-mysql2-adapter
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# vim:fileencoding=utf-8
|
2
|
+
require File.expand_path('../lib/activerecord-mysql2-adapter/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Matthias Viehweger"]
|
6
|
+
gem.email = ["kronn@kronn.de"]
|
7
|
+
gem.description = %q{extracted code from mysql2}
|
8
|
+
gem.summary = %q{extracted code from mysql2}
|
9
|
+
gem.homepage = %q{http://github.com/kronn/activerecord-mysql2-adapter}
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
13
|
+
gem.name = "activerecord-mysql2-adapter"
|
14
|
+
gem.require_paths = ["lib"]
|
15
|
+
gem.version = Activerecord::Mysql2::Adapter::VERSION
|
16
|
+
|
17
|
+
gem.add_dependency 'mysql2'
|
18
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
# AR adapter for using a fibered mysql2 connection with EM
|
4
|
+
# This adapter should be used within Thin or Unicorn with the rack-fiber_pool middleware.
|
5
|
+
# Just update your database.yml's adapter to be 'em_mysql2'
|
6
|
+
|
7
|
+
module ActiveRecord
|
8
|
+
class Base
|
9
|
+
def self.em_mysql2_connection(config)
|
10
|
+
client = ::Mysql2::Fibered::Client.new(config.symbolize_keys)
|
11
|
+
options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0]
|
12
|
+
ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'fiber'
|
18
|
+
require 'eventmachine'
|
19
|
+
require 'mysql2'
|
20
|
+
require 'active_record/connection_adapters/mysql2_adapter'
|
21
|
+
require 'active_record/fiber_patches'
|
22
|
+
|
23
|
+
module Mysql2
|
24
|
+
module Fibered
|
25
|
+
class Client < ::Mysql2::Client
|
26
|
+
module Watcher
|
27
|
+
def initialize(client, deferable)
|
28
|
+
@client = client
|
29
|
+
@deferable = deferable
|
30
|
+
end
|
31
|
+
|
32
|
+
def notify_readable
|
33
|
+
begin
|
34
|
+
detach
|
35
|
+
results = @client.async_result
|
36
|
+
@deferable.succeed(results)
|
37
|
+
rescue Exception => e
|
38
|
+
@deferable.fail(e)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def query(sql, opts={})
|
44
|
+
if ::EM.reactor_running?
|
45
|
+
super(sql, opts.merge(:async => true))
|
46
|
+
deferrable = ::EM::DefaultDeferrable.new
|
47
|
+
::EM.watch(self.socket, Watcher, self, deferrable).notify_readable = true
|
48
|
+
fiber = Fiber.current
|
49
|
+
deferrable.callback do |result|
|
50
|
+
fiber.resume(result)
|
51
|
+
end
|
52
|
+
deferrable.errback do |err|
|
53
|
+
fiber.resume(err)
|
54
|
+
end
|
55
|
+
Fiber.yield.tap do |result|
|
56
|
+
raise result if result.is_a?(Exception)
|
57
|
+
end
|
58
|
+
else
|
59
|
+
super(sql, opts)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,619 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'mysql2'
|
4
|
+
|
5
|
+
module ActiveRecord
|
6
|
+
class Base
|
7
|
+
def self.mysql2_connection(config)
|
8
|
+
config[:username] = 'root' if config[:username].nil?
|
9
|
+
|
10
|
+
if Mysql2::Client.const_defined? :FOUND_ROWS
|
11
|
+
config[:flags] = Mysql2::Client::FOUND_ROWS
|
12
|
+
end
|
13
|
+
|
14
|
+
client = Mysql2::Client.new(config.symbolize_keys)
|
15
|
+
options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0]
|
16
|
+
ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
module ConnectionAdapters
|
21
|
+
class Mysql2IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths) #:nodoc:
|
22
|
+
end
|
23
|
+
|
24
|
+
class Mysql2Column < Column
|
25
|
+
BOOL = "tinyint(1)"
|
26
|
+
def extract_default(default)
|
27
|
+
if sql_type =~ /blob/i || type == :text
|
28
|
+
if default.blank?
|
29
|
+
return null ? nil : ''
|
30
|
+
else
|
31
|
+
raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
|
32
|
+
end
|
33
|
+
elsif missing_default_forged_as_empty_string?(default)
|
34
|
+
nil
|
35
|
+
else
|
36
|
+
super
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def has_default?
|
41
|
+
return false if sql_type =~ /blob/i || type == :text # mysql forbids defaults on blob and text columns
|
42
|
+
super
|
43
|
+
end
|
44
|
+
|
45
|
+
private
|
46
|
+
def simplified_type(field_type)
|
47
|
+
return :boolean if Mysql2Adapter.emulate_booleans && field_type.downcase.index(BOOL)
|
48
|
+
return :string if field_type =~ /enum/i or field_type =~ /set/i
|
49
|
+
return :integer if field_type =~ /year/i
|
50
|
+
return :binary if field_type =~ /bit/i
|
51
|
+
super
|
52
|
+
end
|
53
|
+
|
54
|
+
def extract_limit(sql_type)
|
55
|
+
case sql_type
|
56
|
+
when /blob|text/i
|
57
|
+
case sql_type
|
58
|
+
when /tiny/i
|
59
|
+
255
|
60
|
+
when /medium/i
|
61
|
+
16777215
|
62
|
+
when /long/i
|
63
|
+
2147483647 # mysql only allows 2^31-1, not 2^32-1, somewhat inconsistently with the tiny/medium/normal cases
|
64
|
+
else
|
65
|
+
super # we could return 65535 here, but we leave it undecorated by default
|
66
|
+
end
|
67
|
+
when /^bigint/i; 8
|
68
|
+
when /^int/i; 4
|
69
|
+
when /^mediumint/i; 3
|
70
|
+
when /^smallint/i; 2
|
71
|
+
when /^tinyint/i; 1
|
72
|
+
else
|
73
|
+
super
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# MySQL misreports NOT NULL column default when none is given.
|
78
|
+
# We can't detect this for columns which may have a legitimate ''
|
79
|
+
# default (string) but we can for others (integer, datetime, boolean,
|
80
|
+
# and the rest).
|
81
|
+
#
|
82
|
+
# Test whether the column has default '', is not null, and is not
|
83
|
+
# a type allowing default ''.
|
84
|
+
def missing_default_forged_as_empty_string?(default)
|
85
|
+
type != :string && !null && default == ''
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
class Mysql2Adapter < AbstractAdapter
|
90
|
+
cattr_accessor :emulate_booleans
|
91
|
+
self.emulate_booleans = true
|
92
|
+
|
93
|
+
ADAPTER_NAME = 'Mysql2'
|
94
|
+
PRIMARY = "PRIMARY"
|
95
|
+
|
96
|
+
LOST_CONNECTION_ERROR_MESSAGES = [
|
97
|
+
"Server shutdown in progress",
|
98
|
+
"Broken pipe",
|
99
|
+
"Lost connection to MySQL server during query",
|
100
|
+
"MySQL server has gone away" ]
|
101
|
+
|
102
|
+
QUOTED_TRUE, QUOTED_FALSE = '1', '0'
|
103
|
+
|
104
|
+
NATIVE_DATABASE_TYPES = {
|
105
|
+
:primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
|
106
|
+
:string => { :name => "varchar", :limit => 255 },
|
107
|
+
:text => { :name => "text" },
|
108
|
+
:integer => { :name => "int", :limit => 4 },
|
109
|
+
:float => { :name => "float" },
|
110
|
+
:decimal => { :name => "decimal" },
|
111
|
+
:datetime => { :name => "datetime" },
|
112
|
+
:timestamp => { :name => "datetime" },
|
113
|
+
:time => { :name => "time" },
|
114
|
+
:date => { :name => "date" },
|
115
|
+
:binary => { :name => "blob" },
|
116
|
+
:boolean => { :name => "tinyint", :limit => 1 }
|
117
|
+
}
|
118
|
+
|
119
|
+
def initialize(connection, logger, connection_options, config)
|
120
|
+
super(connection, logger)
|
121
|
+
@connection_options, @config = connection_options, config
|
122
|
+
@quoted_column_names, @quoted_table_names = {}, {}
|
123
|
+
configure_connection
|
124
|
+
end
|
125
|
+
|
126
|
+
def adapter_name
|
127
|
+
ADAPTER_NAME
|
128
|
+
end
|
129
|
+
|
130
|
+
def supports_migrations?
|
131
|
+
true
|
132
|
+
end
|
133
|
+
|
134
|
+
def supports_primary_key?
|
135
|
+
true
|
136
|
+
end
|
137
|
+
|
138
|
+
def supports_savepoints?
|
139
|
+
true
|
140
|
+
end
|
141
|
+
|
142
|
+
def native_database_types
|
143
|
+
NATIVE_DATABASE_TYPES
|
144
|
+
end
|
145
|
+
|
146
|
+
# QUOTING ==================================================
|
147
|
+
|
148
|
+
def quote(value, column = nil)
|
149
|
+
if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
|
150
|
+
s = column.class.string_to_binary(value).unpack("H*")[0]
|
151
|
+
"x'#{s}'"
|
152
|
+
elsif value.kind_of?(BigDecimal)
|
153
|
+
value.to_s("F")
|
154
|
+
else
|
155
|
+
super
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def quote_column_name(name) #:nodoc:
|
160
|
+
@quoted_column_names[name] ||= "`#{name.to_s.gsub('`', '``')}`"
|
161
|
+
end
|
162
|
+
|
163
|
+
def quote_table_name(name) #:nodoc:
|
164
|
+
@quoted_table_names[name] ||= quote_column_name(name).gsub('.', '`.`')
|
165
|
+
end
|
166
|
+
|
167
|
+
def quote_string(string)
|
168
|
+
@connection.escape(string)
|
169
|
+
end
|
170
|
+
|
171
|
+
def quoted_true
|
172
|
+
QUOTED_TRUE
|
173
|
+
end
|
174
|
+
|
175
|
+
def quoted_false
|
176
|
+
QUOTED_FALSE
|
177
|
+
end
|
178
|
+
|
179
|
+
# REFERENTIAL INTEGRITY ====================================
|
180
|
+
|
181
|
+
def disable_referential_integrity(&block) #:nodoc:
|
182
|
+
old = select_value("SELECT @@FOREIGN_KEY_CHECKS")
|
183
|
+
|
184
|
+
begin
|
185
|
+
update("SET FOREIGN_KEY_CHECKS = 0")
|
186
|
+
yield
|
187
|
+
ensure
|
188
|
+
update("SET FOREIGN_KEY_CHECKS = #{old}")
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# CONNECTION MANAGEMENT ====================================
|
193
|
+
|
194
|
+
def active?
|
195
|
+
return false unless @connection
|
196
|
+
@connection.ping
|
197
|
+
end
|
198
|
+
|
199
|
+
def reconnect!
|
200
|
+
disconnect!
|
201
|
+
connect
|
202
|
+
end
|
203
|
+
|
204
|
+
# this is set to true in 2.3, but we don't want it to be
|
205
|
+
def requires_reloading?
|
206
|
+
false
|
207
|
+
end
|
208
|
+
|
209
|
+
def disconnect!
|
210
|
+
unless @connection.nil?
|
211
|
+
@connection.close
|
212
|
+
@connection = nil
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
def reset!
|
217
|
+
disconnect!
|
218
|
+
connect
|
219
|
+
end
|
220
|
+
|
221
|
+
# DATABASE STATEMENTS ======================================
|
222
|
+
|
223
|
+
# FIXME: re-enable the following once a "better" query_cache solution is in core
|
224
|
+
#
|
225
|
+
# The overrides below perform much better than the originals in AbstractAdapter
|
226
|
+
# because we're able to take advantage of mysql2's lazy-loading capabilities
|
227
|
+
#
|
228
|
+
# # Returns a record hash with the column names as keys and column values
|
229
|
+
# # as values.
|
230
|
+
# def select_one(sql, name = nil)
|
231
|
+
# result = execute(sql, name)
|
232
|
+
# result.each(:as => :hash) do |r|
|
233
|
+
# return r
|
234
|
+
# end
|
235
|
+
# end
|
236
|
+
#
|
237
|
+
# # Returns a single value from a record
|
238
|
+
# def select_value(sql, name = nil)
|
239
|
+
# result = execute(sql, name)
|
240
|
+
# if first = result.first
|
241
|
+
# first.first
|
242
|
+
# end
|
243
|
+
# end
|
244
|
+
#
|
245
|
+
# # Returns an array of the values of the first column in a select:
|
246
|
+
# # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
|
247
|
+
# def select_values(sql, name = nil)
|
248
|
+
# execute(sql, name).map { |row| row.first }
|
249
|
+
# end
|
250
|
+
|
251
|
+
# Returns an array of arrays containing the field values.
|
252
|
+
# Order is the same as that returned by +columns+.
|
253
|
+
def select_rows(sql, name = nil)
|
254
|
+
execute(sql, name).to_a
|
255
|
+
end
|
256
|
+
|
257
|
+
# Executes the SQL statement in the context of this connection.
|
258
|
+
def execute(sql, name = nil)
|
259
|
+
# make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
|
260
|
+
# made since we established the connection
|
261
|
+
@connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
|
262
|
+
if name == :skip_logging
|
263
|
+
@connection.query(sql)
|
264
|
+
else
|
265
|
+
log(sql, name) { @connection.query(sql) }
|
266
|
+
end
|
267
|
+
rescue ActiveRecord::StatementInvalid => exception
|
268
|
+
if exception.message.split(":").first =~ /Packets out of order/
|
269
|
+
raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
|
270
|
+
else
|
271
|
+
raise
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil)
|
276
|
+
super
|
277
|
+
id_value || @connection.last_id
|
278
|
+
end
|
279
|
+
alias :create :insert_sql
|
280
|
+
|
281
|
+
def update_sql(sql, name = nil)
|
282
|
+
super
|
283
|
+
@connection.affected_rows
|
284
|
+
end
|
285
|
+
|
286
|
+
def begin_db_transaction
|
287
|
+
execute "BEGIN"
|
288
|
+
rescue Exception
|
289
|
+
# Transactions aren't supported
|
290
|
+
end
|
291
|
+
|
292
|
+
def commit_db_transaction
|
293
|
+
execute "COMMIT"
|
294
|
+
rescue Exception
|
295
|
+
# Transactions aren't supported
|
296
|
+
end
|
297
|
+
|
298
|
+
def rollback_db_transaction
|
299
|
+
execute "ROLLBACK"
|
300
|
+
rescue Exception
|
301
|
+
# Transactions aren't supported
|
302
|
+
end
|
303
|
+
|
304
|
+
def create_savepoint
|
305
|
+
execute("SAVEPOINT #{current_savepoint_name}")
|
306
|
+
end
|
307
|
+
|
308
|
+
def rollback_to_savepoint
|
309
|
+
execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
|
310
|
+
end
|
311
|
+
|
312
|
+
def release_savepoint
|
313
|
+
execute("RELEASE SAVEPOINT #{current_savepoint_name}")
|
314
|
+
end
|
315
|
+
|
316
|
+
def add_limit_offset!(sql, options)
|
317
|
+
limit, offset = options[:limit], options[:offset]
|
318
|
+
if limit && offset
|
319
|
+
sql << " LIMIT #{offset.to_i}, #{sanitize_limit(limit)}"
|
320
|
+
elsif limit
|
321
|
+
sql << " LIMIT #{sanitize_limit(limit)}"
|
322
|
+
elsif offset
|
323
|
+
sql << " OFFSET #{offset.to_i}"
|
324
|
+
end
|
325
|
+
sql
|
326
|
+
end
|
327
|
+
|
328
|
+
# SCHEMA STATEMENTS ========================================
|
329
|
+
|
330
|
+
def structure_dump
|
331
|
+
if supports_views?
|
332
|
+
sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
|
333
|
+
else
|
334
|
+
sql = "SHOW TABLES"
|
335
|
+
end
|
336
|
+
|
337
|
+
select_all(sql).inject("") do |structure, table|
|
338
|
+
table.delete('Table_type')
|
339
|
+
structure += select_one("SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}")["Create Table"] + ";\n\n"
|
340
|
+
end
|
341
|
+
end
|
342
|
+
|
343
|
+
def recreate_database(name, options = {})
|
344
|
+
drop_database(name)
|
345
|
+
create_database(name, options)
|
346
|
+
end
|
347
|
+
|
348
|
+
# Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
|
349
|
+
# Charset defaults to utf8.
|
350
|
+
#
|
351
|
+
# Example:
|
352
|
+
# create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin'
|
353
|
+
# create_database 'matt_development'
|
354
|
+
# create_database 'matt_development', :charset => :big5
|
355
|
+
def create_database(name, options = {})
|
356
|
+
if options[:collation]
|
357
|
+
execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
|
358
|
+
else
|
359
|
+
execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}`"
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
def drop_database(name) #:nodoc:
|
364
|
+
execute "DROP DATABASE IF EXISTS `#{name}`"
|
365
|
+
end
|
366
|
+
|
367
|
+
def current_database
|
368
|
+
select_value 'SELECT DATABASE() as db'
|
369
|
+
end
|
370
|
+
|
371
|
+
# Returns the database character set.
|
372
|
+
def charset
|
373
|
+
show_variable 'character_set_database'
|
374
|
+
end
|
375
|
+
|
376
|
+
# Returns the database collation strategy.
|
377
|
+
def collation
|
378
|
+
show_variable 'collation_database'
|
379
|
+
end
|
380
|
+
|
381
|
+
def tables(name = nil)
|
382
|
+
tables = []
|
383
|
+
execute("SHOW TABLES", name).each do |field|
|
384
|
+
tables << field.first
|
385
|
+
end
|
386
|
+
tables
|
387
|
+
end
|
388
|
+
|
389
|
+
def table_exists?(name)
|
390
|
+
return true if super
|
391
|
+
|
392
|
+
name = name.to_s
|
393
|
+
schema, table = name.split('.', 2)
|
394
|
+
|
395
|
+
unless table # A table was provided without a schema
|
396
|
+
table = schema
|
397
|
+
schema = nil
|
398
|
+
end
|
399
|
+
|
400
|
+
tables(nil, schema).include? table
|
401
|
+
end
|
402
|
+
|
403
|
+
def drop_table(table_name, options = {})
|
404
|
+
super(table_name, options)
|
405
|
+
end
|
406
|
+
|
407
|
+
def indexes(table_name, name = nil)
|
408
|
+
indexes = []
|
409
|
+
current_index = nil
|
410
|
+
result = execute("SHOW KEYS FROM #{quote_table_name(table_name)}", name)
|
411
|
+
result.each(:symbolize_keys => true, :as => :hash) do |row|
|
412
|
+
if current_index != row[:Key_name]
|
413
|
+
next if row[:Key_name] == PRIMARY # skip the primary key
|
414
|
+
current_index = row[:Key_name]
|
415
|
+
indexes << Mysql2IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique] == 0, [], [])
|
416
|
+
end
|
417
|
+
|
418
|
+
indexes.last.columns << row[:Column_name]
|
419
|
+
indexes.last.lengths << row[:Sub_part]
|
420
|
+
end
|
421
|
+
indexes
|
422
|
+
end
|
423
|
+
|
424
|
+
def columns(table_name, name = nil)
|
425
|
+
sql = "SHOW FIELDS FROM #{quote_table_name(table_name)}"
|
426
|
+
columns = []
|
427
|
+
result = execute(sql, :skip_logging)
|
428
|
+
result.each(:symbolize_keys => true, :as => :hash) { |field|
|
429
|
+
columns << Mysql2Column.new(field[:Field], field[:Default], field[:Type], field[:Null] == "YES")
|
430
|
+
}
|
431
|
+
columns
|
432
|
+
end
|
433
|
+
|
434
|
+
def create_table(table_name, options = {})
|
435
|
+
super(table_name, options.reverse_merge(:options => "ENGINE=InnoDB"))
|
436
|
+
end
|
437
|
+
|
438
|
+
def rename_table(table_name, new_name)
|
439
|
+
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
|
440
|
+
end
|
441
|
+
|
442
|
+
def add_column(table_name, column_name, type, options = {})
|
443
|
+
add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
444
|
+
add_column_options!(add_column_sql, options)
|
445
|
+
add_column_position!(add_column_sql, options)
|
446
|
+
execute(add_column_sql)
|
447
|
+
end
|
448
|
+
|
449
|
+
def change_column_default(table_name, column_name, default)
|
450
|
+
column = column_for(table_name, column_name)
|
451
|
+
change_column table_name, column_name, column.sql_type, :default => default
|
452
|
+
end
|
453
|
+
|
454
|
+
def change_column_null(table_name, column_name, null, default = nil)
|
455
|
+
column = column_for(table_name, column_name)
|
456
|
+
|
457
|
+
unless null || default.nil?
|
458
|
+
execute("UPDATE #{quote_table_name(table_name)} SET #{quote_column_name(column_name)}=#{quote(default)} WHERE #{quote_column_name(column_name)} IS NULL")
|
459
|
+
end
|
460
|
+
|
461
|
+
change_column table_name, column_name, column.sql_type, :null => null
|
462
|
+
end
|
463
|
+
|
464
|
+
def change_column(table_name, column_name, type, options = {})
|
465
|
+
column = column_for(table_name, column_name)
|
466
|
+
|
467
|
+
unless options_include_default?(options)
|
468
|
+
options[:default] = column.default
|
469
|
+
end
|
470
|
+
|
471
|
+
unless options.has_key?(:null)
|
472
|
+
options[:null] = column.null
|
473
|
+
end
|
474
|
+
|
475
|
+
change_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
476
|
+
add_column_options!(change_column_sql, options)
|
477
|
+
add_column_position!(change_column_sql, options)
|
478
|
+
execute(change_column_sql)
|
479
|
+
end
|
480
|
+
|
481
|
+
def rename_column(table_name, column_name, new_column_name)
|
482
|
+
options = {}
|
483
|
+
if column = columns(table_name).find { |c| c.name == column_name.to_s }
|
484
|
+
options[:default] = column.default
|
485
|
+
options[:null] = column.null
|
486
|
+
else
|
487
|
+
raise ActiveRecordError, "No such column: #{table_name}.#{column_name}"
|
488
|
+
end
|
489
|
+
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'")["Type"]
|
490
|
+
rename_column_sql = "ALTER TABLE #{quote_table_name(table_name)} CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
|
491
|
+
add_column_options!(rename_column_sql, options)
|
492
|
+
execute(rename_column_sql)
|
493
|
+
end
|
494
|
+
|
495
|
+
# Maps logical Rails types to MySQL-specific data types.
|
496
|
+
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
|
497
|
+
return super unless type.to_s == 'integer'
|
498
|
+
|
499
|
+
case limit
|
500
|
+
when 1; 'tinyint'
|
501
|
+
when 2; 'smallint'
|
502
|
+
when 3; 'mediumint'
|
503
|
+
when nil, 4, 11; 'int(11)' # compatibility with MySQL default
|
504
|
+
when 5..8; 'bigint'
|
505
|
+
else raise(ActiveRecordError, "No integer type has byte size #{limit}")
|
506
|
+
end
|
507
|
+
end
|
508
|
+
|
509
|
+
def add_column_position!(sql, options)
|
510
|
+
if options[:first]
|
511
|
+
sql << " FIRST"
|
512
|
+
elsif options[:after]
|
513
|
+
sql << " AFTER #{quote_column_name(options[:after])}"
|
514
|
+
end
|
515
|
+
end
|
516
|
+
|
517
|
+
def show_variable(name)
|
518
|
+
variables = select_all("SHOW VARIABLES LIKE '#{name}'")
|
519
|
+
variables.first['Value'] unless variables.empty?
|
520
|
+
end
|
521
|
+
|
522
|
+
def pk_and_sequence_for(table)
|
523
|
+
keys = []
|
524
|
+
result = execute("describe #{quote_table_name(table)}")
|
525
|
+
result.each(:symbolize_keys => true, :as => :hash) do |row|
|
526
|
+
keys << row[:Field] if row[:Key] == "PRI"
|
527
|
+
end
|
528
|
+
keys.length == 1 ? [keys.first, nil] : nil
|
529
|
+
end
|
530
|
+
|
531
|
+
# Returns just a table's primary key
|
532
|
+
def primary_key(table)
|
533
|
+
pk_and_sequence = pk_and_sequence_for(table)
|
534
|
+
pk_and_sequence && pk_and_sequence.first
|
535
|
+
end
|
536
|
+
|
537
|
+
def case_sensitive_equality_operator
|
538
|
+
"= BINARY"
|
539
|
+
end
|
540
|
+
|
541
|
+
def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key)
|
542
|
+
where_sql
|
543
|
+
end
|
544
|
+
|
545
|
+
protected
|
546
|
+
def quoted_columns_for_index(column_names, options = {})
|
547
|
+
length = options[:length] if options.is_a?(Hash)
|
548
|
+
|
549
|
+
quoted_column_names = case length
|
550
|
+
when Hash
|
551
|
+
column_names.map {|name| length[name] ? "#{quote_column_name(name)}(#{length[name]})" : quote_column_name(name) }
|
552
|
+
when Fixnum
|
553
|
+
column_names.map {|name| "#{quote_column_name(name)}(#{length})"}
|
554
|
+
else
|
555
|
+
column_names.map {|name| quote_column_name(name) }
|
556
|
+
end
|
557
|
+
end
|
558
|
+
|
559
|
+
def translate_exception(exception, message)
|
560
|
+
return super unless exception.respond_to?(:error_number)
|
561
|
+
|
562
|
+
case exception.error_number
|
563
|
+
when 1062
|
564
|
+
RecordNotUnique.new(message, exception)
|
565
|
+
when 1452
|
566
|
+
InvalidForeignKey.new(message, exception)
|
567
|
+
else
|
568
|
+
super
|
569
|
+
end
|
570
|
+
end
|
571
|
+
|
572
|
+
private
|
573
|
+
def connect
|
574
|
+
@connection = Mysql2::Client.new(@config)
|
575
|
+
configure_connection
|
576
|
+
end
|
577
|
+
|
578
|
+
def configure_connection
|
579
|
+
@connection.query_options.merge!(:as => :array)
|
580
|
+
|
581
|
+
# By default, MySQL 'where id is null' selects the last inserted id.
|
582
|
+
# Turn this off. http://dev.rubyonrails.org/ticket/6778
|
583
|
+
variable_assignments = ['SQL_AUTO_IS_NULL=0']
|
584
|
+
encoding = @config[:encoding]
|
585
|
+
|
586
|
+
# make sure we set the encoding
|
587
|
+
variable_assignments << "NAMES '#{encoding}'" if encoding
|
588
|
+
|
589
|
+
# increase timeout so mysql server doesn't disconnect us
|
590
|
+
wait_timeout = @config[:wait_timeout]
|
591
|
+
wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum)
|
592
|
+
variable_assignments << "@@wait_timeout = #{wait_timeout}"
|
593
|
+
|
594
|
+
execute("SET #{variable_assignments.join(', ')}", :skip_logging)
|
595
|
+
end
|
596
|
+
|
597
|
+
# Returns an array of record hashes with the column names as keys and
|
598
|
+
# column values as values.
|
599
|
+
def select(sql, name = nil)
|
600
|
+
execute(sql, name).each(:as => :hash)
|
601
|
+
end
|
602
|
+
|
603
|
+
def supports_views?
|
604
|
+
version[0] >= 5
|
605
|
+
end
|
606
|
+
|
607
|
+
def version
|
608
|
+
@version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
|
609
|
+
end
|
610
|
+
|
611
|
+
def column_for(table_name, column_name)
|
612
|
+
unless column = columns(table_name).find { |c| c.name == column_name.to_s }
|
613
|
+
raise "No such column: #{table_name}.#{column_name}"
|
614
|
+
end
|
615
|
+
column
|
616
|
+
end
|
617
|
+
end
|
618
|
+
end
|
619
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# Necessary monkeypatching to make AR fiber-friendly.
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module ConnectionAdapters
|
5
|
+
|
6
|
+
def self.fiber_pools
|
7
|
+
@fiber_pools ||= []
|
8
|
+
end
|
9
|
+
def self.register_fiber_pool(fp)
|
10
|
+
fiber_pools << fp
|
11
|
+
end
|
12
|
+
|
13
|
+
class FiberedMonitor
|
14
|
+
class Queue
|
15
|
+
def initialize
|
16
|
+
@queue = []
|
17
|
+
end
|
18
|
+
|
19
|
+
def wait(timeout)
|
20
|
+
t = timeout || 5
|
21
|
+
fiber = Fiber.current
|
22
|
+
x = EM::Timer.new(t) do
|
23
|
+
@queue.delete(fiber)
|
24
|
+
fiber.resume(false)
|
25
|
+
end
|
26
|
+
@queue << fiber
|
27
|
+
Fiber.yield.tap do
|
28
|
+
x.cancel
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def signal
|
33
|
+
fiber = @queue.pop
|
34
|
+
fiber.resume(true) if fiber
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def synchronize
|
39
|
+
yield
|
40
|
+
end
|
41
|
+
|
42
|
+
def new_cond
|
43
|
+
Queue.new
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# ActiveRecord's connection pool is based on threads. Since we are working
|
48
|
+
# with EM and a single thread, multiple fiber design, we need to provide
|
49
|
+
# our own connection pool that keys off of Fiber.current so that different
|
50
|
+
# fibers running in the same thread don't try to use the same connection.
|
51
|
+
class ConnectionPool
|
52
|
+
def initialize(spec)
|
53
|
+
@spec = spec
|
54
|
+
|
55
|
+
# The cache of reserved connections mapped to threads
|
56
|
+
@reserved_connections = {}
|
57
|
+
|
58
|
+
# The mutex used to synchronize pool access
|
59
|
+
@connection_mutex = FiberedMonitor.new
|
60
|
+
@queue = @connection_mutex.new_cond
|
61
|
+
|
62
|
+
# default 5 second timeout unless on ruby 1.9
|
63
|
+
@timeout = spec.config[:wait_timeout] || 5
|
64
|
+
|
65
|
+
# default max pool size to 5
|
66
|
+
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
|
67
|
+
|
68
|
+
@connections = []
|
69
|
+
@checked_out = []
|
70
|
+
@automatic_reconnect = true
|
71
|
+
@tables = {}
|
72
|
+
|
73
|
+
@columns = Hash.new do |h, table_name|
|
74
|
+
h[table_name] = with_connection do |conn|
|
75
|
+
|
76
|
+
# Fetch a list of columns
|
77
|
+
conn.columns(table_name, "#{table_name} Columns").tap do |columns|
|
78
|
+
|
79
|
+
# set primary key information
|
80
|
+
columns.each do |column|
|
81
|
+
column.primary = column.name == primary_keys[table_name]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
@columns_hash = Hash.new do |h, table_name|
|
88
|
+
h[table_name] = Hash[columns[table_name].map { |col|
|
89
|
+
[col.name, col]
|
90
|
+
}]
|
91
|
+
end
|
92
|
+
|
93
|
+
@primary_keys = Hash.new do |h, table_name|
|
94
|
+
h[table_name] = with_connection do |conn|
|
95
|
+
table_exists?(table_name) ? conn.primary_key(table_name) : 'id'
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def clear_stale_cached_connections!
|
101
|
+
cache = @reserved_connections
|
102
|
+
keys = Set.new(cache.keys)
|
103
|
+
|
104
|
+
ActiveRecord::ConnectionAdapters.fiber_pools.each do |pool|
|
105
|
+
pool.busy_fibers.each_pair do |object_id, fiber|
|
106
|
+
keys.delete(object_id)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
keys.each do |key|
|
111
|
+
next unless cache.has_key?(key)
|
112
|
+
checkin cache[key]
|
113
|
+
cache.delete(key)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
|
119
|
+
def current_connection_id #:nodoc:
|
120
|
+
Fiber.current.object_id
|
121
|
+
end
|
122
|
+
|
123
|
+
def checkout_and_verify(c)
|
124
|
+
@checked_out << c
|
125
|
+
c.run_callbacks :checkout
|
126
|
+
c.verify!
|
127
|
+
c
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
132
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: activerecord-mysql2-adapter
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Matthias Viehweger
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-03 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: mysql2
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
description: extracted code from mysql2
|
31
|
+
email:
|
32
|
+
- kronn@kronn.de
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- .gitignore
|
38
|
+
- .rvmrc
|
39
|
+
- Gemfile
|
40
|
+
- LICENSE
|
41
|
+
- README.md
|
42
|
+
- Rakefile
|
43
|
+
- activerecord-mysql2-adapter.gemspec
|
44
|
+
- lib/active_record/connection_adapters/em_mysql2_adapter.rb
|
45
|
+
- lib/active_record/connection_adapters/mysql2_adapter.rb
|
46
|
+
- lib/active_record/fiber_patches.rb
|
47
|
+
- lib/activerecord-mysql2-adapter.rb
|
48
|
+
- lib/activerecord-mysql2-adapter/version.rb
|
49
|
+
- lib/arel/engines/sql/compilers/mysql2_compiler.rb
|
50
|
+
homepage: http://github.com/kronn/activerecord-mysql2-adapter
|
51
|
+
licenses: []
|
52
|
+
post_install_message:
|
53
|
+
rdoc_options: []
|
54
|
+
require_paths:
|
55
|
+
- lib
|
56
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
63
|
+
none: false
|
64
|
+
requirements:
|
65
|
+
- - ! '>='
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '0'
|
68
|
+
requirements: []
|
69
|
+
rubyforge_project:
|
70
|
+
rubygems_version: 1.8.24
|
71
|
+
signing_key:
|
72
|
+
specification_version: 3
|
73
|
+
summary: extracted code from mysql2
|
74
|
+
test_files: []
|