activerecord-mysql2-adapter 0.0.2
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.
- 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: []
|