mysql_replication_adapter 0.1.0
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/History.txt +4 -0
- data/License.txt +20 -0
- data/Manifest.txt +16 -0
- data/README.txt +3 -0
- data/Rakefile +123 -0
- data/lib/mysql_replication_adapter.rb +520 -0
- data/lib/mysql_replication_adapter/version.rb +9 -0
- data/scripts/txt2html +67 -0
- data/setup.rb +1585 -0
- data/test/test_helper.rb +2 -0
- data/test/test_mysql_replication_adapter.rb +11 -0
- data/website/index.html +92 -0
- data/website/index.txt +38 -0
- data/website/javascripts/rounded_corners_lite.inc.js +285 -0
- data/website/stylesheets/screen.css +138 -0
- data/website/template.rhtml +48 -0
- metadata +67 -0
data/History.txt
ADDED
data/License.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2007 FIXME full name
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Manifest.txt
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
History.txt
|
2
|
+
License.txt
|
3
|
+
Manifest.txt
|
4
|
+
README.txt
|
5
|
+
Rakefile
|
6
|
+
lib/mysql_replication_adapter.rb
|
7
|
+
lib/mysql_replication_adapter/version.rb
|
8
|
+
scripts/txt2html
|
9
|
+
setup.rb
|
10
|
+
test/test_helper.rb
|
11
|
+
test/test_mysql_replication_adapter.rb
|
12
|
+
website/index.html
|
13
|
+
website/index.txt
|
14
|
+
website/javascripts/rounded_corners_lite.inc.js
|
15
|
+
website/stylesheets/screen.css
|
16
|
+
website/template.rhtml
|
data/README.txt
ADDED
data/Rakefile
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
require 'rake/clean'
|
4
|
+
require 'rake/testtask'
|
5
|
+
require 'rake/packagetask'
|
6
|
+
require 'rake/gempackagetask'
|
7
|
+
require 'rake/rdoctask'
|
8
|
+
require 'rake/contrib/rubyforgepublisher'
|
9
|
+
require 'fileutils'
|
10
|
+
require 'hoe'
|
11
|
+
|
12
|
+
include FileUtils
|
13
|
+
require File.join(File.dirname(__FILE__), 'lib', 'mysql_replication_adapter', 'version')
|
14
|
+
|
15
|
+
AUTHOR = 'Bryan Duxbury' # can also be an array of Authors
|
16
|
+
EMAIL = "bryan@rapleaf.com"
|
17
|
+
DESCRIPTION = "An ActiveRecord database adapter that allows you to specify a single write master and multiple read-only slaves."
|
18
|
+
GEM_NAME = 'mysql_replication_adapter' # what ppl will type to install your gem
|
19
|
+
|
20
|
+
@config_file = "~/.rubyforge/user-config.yml"
|
21
|
+
@config = nil
|
22
|
+
def rubyforge_username
|
23
|
+
unless @config
|
24
|
+
begin
|
25
|
+
@config = YAML.load(File.read(File.expand_path(@config_file)))
|
26
|
+
rescue
|
27
|
+
puts <<-EOS
|
28
|
+
ERROR: No rubyforge config file found: #{@config_file}"
|
29
|
+
Run 'rubyforge setup' to prepare your env for access to Rubyforge
|
30
|
+
- See http://newgem.rubyforge.org/rubyforge.html for more details
|
31
|
+
EOS
|
32
|
+
exit
|
33
|
+
end
|
34
|
+
end
|
35
|
+
@rubyforge_username ||= @config["username"]
|
36
|
+
end
|
37
|
+
|
38
|
+
RUBYFORGE_PROJECT = 'mysql-replicate' # The unix name for your project
|
39
|
+
HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
|
40
|
+
DOWNLOAD_PATH = "http://rubyforge.org/projects/#{RUBYFORGE_PROJECT}"
|
41
|
+
|
42
|
+
NAME = "mysql_replication_adapter"
|
43
|
+
REV = nil
|
44
|
+
# UNCOMMENT IF REQUIRED:
|
45
|
+
# REV = `svn info`.each {|line| if line =~ /^Revision:/ then k,v = line.split(': '); break v.chomp; else next; end} rescue nil
|
46
|
+
VERS = MysqlReplicationAdapter::VERSION::STRING + (REV ? ".#{REV}" : "")
|
47
|
+
CLEAN.include ['**/.*.sw?', '*.gem', '.config', '**/.DS_Store']
|
48
|
+
RDOC_OPTS = ['--quiet', '--title', 'mysql_replication_adapter documentation',
|
49
|
+
"--opname", "index.html",
|
50
|
+
"--line-numbers",
|
51
|
+
"--main", "README",
|
52
|
+
"--inline-source"]
|
53
|
+
|
54
|
+
class Hoe
|
55
|
+
def extra_deps
|
56
|
+
@extra_deps.reject { |x| Array(x).first == 'hoe' }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Generate all the Rake tasks
|
61
|
+
# Run 'rake -T' to see list of generated tasks (from gem root directory)
|
62
|
+
hoe = Hoe.new(GEM_NAME, VERS) do |p|
|
63
|
+
p.author = AUTHOR
|
64
|
+
p.description = DESCRIPTION
|
65
|
+
p.email = EMAIL
|
66
|
+
p.summary = DESCRIPTION
|
67
|
+
p.url = HOMEPATH
|
68
|
+
p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
|
69
|
+
p.test_globs = ["test/**/test_*.rb"]
|
70
|
+
p.clean_globs |= CLEAN #An array of file patterns to delete on clean.
|
71
|
+
|
72
|
+
# == Optional
|
73
|
+
p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
|
74
|
+
#p.extra_deps = [] # An array of rubygem dependencies [name, version], e.g. [ ['active_support', '>= 1.3.1'] ]
|
75
|
+
#p.spec_extras = {} # A hash of extra values to set in the gemspec.
|
76
|
+
end
|
77
|
+
|
78
|
+
CHANGES = hoe.paragraphs_of('History.txt', 0..1).join("\n\n")
|
79
|
+
PATH = (RUBYFORGE_PROJECT == GEM_NAME) ? RUBYFORGE_PROJECT : "#{RUBYFORGE_PROJECT}/#{GEM_NAME}"
|
80
|
+
hoe.remote_rdoc_dir = File.join(PATH.gsub(/^#{RUBYFORGE_PROJECT}\/?/,''), 'rdoc')
|
81
|
+
|
82
|
+
desc 'Generate website files'
|
83
|
+
task :website_generate do
|
84
|
+
Dir['website/**/*.txt'].each do |txt|
|
85
|
+
sh %{ ruby scripts/txt2html #{txt} > #{txt.gsub(/txt$/,'html')} }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
desc 'Upload website files to rubyforge'
|
90
|
+
task :website_upload do
|
91
|
+
host = "#{rubyforge_username}@rubyforge.org"
|
92
|
+
remote_dir = "/var/www/gforge-projects/#{PATH}/"
|
93
|
+
local_dir = 'website'
|
94
|
+
sh %{rsync -aCv #{local_dir}/ #{host}:#{remote_dir}}
|
95
|
+
end
|
96
|
+
|
97
|
+
desc 'Generate and upload website files'
|
98
|
+
task :website => [:website_generate, :website_upload, :publish_docs]
|
99
|
+
|
100
|
+
desc 'Release the website and new gem version'
|
101
|
+
task :deploy => [:check_version, :website, :release] do
|
102
|
+
puts "Remember to create SVN tag:"
|
103
|
+
puts "svn copy svn+ssh://#{rubyforge_username}@rubyforge.org/var/svn/#{PATH}/trunk " +
|
104
|
+
"svn+ssh://#{rubyforge_username}@rubyforge.org/var/svn/#{PATH}/tags/REL-#{VERS} "
|
105
|
+
puts "Suggested comment:"
|
106
|
+
puts "Tagging release #{CHANGES}"
|
107
|
+
end
|
108
|
+
|
109
|
+
desc 'Runs tasks website_generate and install_gem as a local deployment of the gem'
|
110
|
+
task :local_deploy => [:website_generate, :install_gem]
|
111
|
+
|
112
|
+
task :check_version do
|
113
|
+
unless ENV['VERSION']
|
114
|
+
puts 'Must pass a VERSION=x.y.z release version'
|
115
|
+
exit
|
116
|
+
end
|
117
|
+
unless ENV['VERSION'] == VERS
|
118
|
+
puts "Please update your version.rb to match the release version, currently #{VERS}"
|
119
|
+
exit
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
|
@@ -0,0 +1,520 @@
|
|
1
|
+
module MysqlReplicationAdapter
|
2
|
+
end
|
3
|
+
|
4
|
+
require 'mysql_replication_adapter/version'
|
5
|
+
|
6
|
+
unless defined?(RAILS_CONNECTION_ADAPTERS) && RAILS_CONNECTION_ADAPTERS.include?("mysql_replication")
|
7
|
+
require 'active_record' unless defined?(RAILS_CONNECTION_ADAPTERS)
|
8
|
+
RAILS_CONNECTION_ADAPTERS << "mysql_replication"
|
9
|
+
end
|
10
|
+
|
11
|
+
require 'active_record/connection_adapters/abstract_adapter'
|
12
|
+
require 'set'
|
13
|
+
|
14
|
+
module MysqlCompat #:nodoc:
|
15
|
+
# add all_hashes method to standard mysql-c bindings or pure ruby version
|
16
|
+
def self.define_all_hashes_method!
|
17
|
+
raise 'Mysql not loaded' unless defined?(::Mysql)
|
18
|
+
|
19
|
+
target = defined?(Mysql::Result) ? Mysql::Result : MysqlRes
|
20
|
+
return if target.instance_methods.include?('all_hashes')
|
21
|
+
|
22
|
+
# Ruby driver has a version string and returns null values in each_hash
|
23
|
+
# C driver >= 2.7 returns null values in each_hash
|
24
|
+
if Mysql.const_defined?(:VERSION) && (Mysql::VERSION.is_a?(String) || Mysql::VERSION >= 20700)
|
25
|
+
target.class_eval <<-'end_eval'
|
26
|
+
def all_hashes
|
27
|
+
rows = []
|
28
|
+
each_hash { |row| rows << row }
|
29
|
+
rows
|
30
|
+
end
|
31
|
+
end_eval
|
32
|
+
|
33
|
+
# adapters before 2.7 don't have a version constant
|
34
|
+
# and don't return null values in each_hash
|
35
|
+
else
|
36
|
+
target.class_eval <<-'end_eval'
|
37
|
+
def all_hashes
|
38
|
+
rows = []
|
39
|
+
all_fields = fetch_fields.inject({}) { |fields, f| fields[f.name] = nil; fields }
|
40
|
+
each_hash { |row| rows << all_fields.dup.update(row) }
|
41
|
+
rows
|
42
|
+
end
|
43
|
+
end_eval
|
44
|
+
end
|
45
|
+
|
46
|
+
unless target.instance_methods.include?('all_hashes')
|
47
|
+
raise "Failed to defined #{target.name}#all_hashes method. Mysql::VERSION = #{Mysql::VERSION.inspect}"
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
module ActiveRecord
|
53
|
+
class Base
|
54
|
+
def self.require_mysql
|
55
|
+
# Include the MySQL driver if one hasn't already been loaded
|
56
|
+
unless defined? Mysql
|
57
|
+
begin
|
58
|
+
require_library_or_gem 'mysql'
|
59
|
+
rescue LoadError => cannot_require_mysql
|
60
|
+
# Use the bundled Ruby/MySQL driver if no driver is already in place
|
61
|
+
begin
|
62
|
+
require 'active_record/vendor/mysql'
|
63
|
+
rescue LoadError
|
64
|
+
raise cannot_require_mysql
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
# Define Mysql::Result.all_hashes
|
70
|
+
MysqlCompat.define_all_hashes_method!
|
71
|
+
end
|
72
|
+
|
73
|
+
# Establishes a connection to the database that's used by all Active Record objects.
|
74
|
+
def self.mysql_replication_connection(config) # :nodoc:
|
75
|
+
config = config.symbolize_keys
|
76
|
+
host = config[:host]
|
77
|
+
port = config[:port]
|
78
|
+
socket = config[:socket]
|
79
|
+
username = config[:username] ? config[:username].to_s : 'root'
|
80
|
+
password = config[:password].to_s
|
81
|
+
|
82
|
+
if config.has_key?(:database)
|
83
|
+
database = config[:database]
|
84
|
+
else
|
85
|
+
raise ArgumentError, "No database specified. Missing argument: database."
|
86
|
+
end
|
87
|
+
|
88
|
+
require_mysql
|
89
|
+
mysql = Mysql.init
|
90
|
+
mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslkey]
|
91
|
+
|
92
|
+
ConnectionAdapters::MysqlReplicationAdapter.new(mysql, logger, [host, username, password, database, port, socket], config)
|
93
|
+
end
|
94
|
+
|
95
|
+
class << self
|
96
|
+
alias_method :old_find_every, :find_every
|
97
|
+
|
98
|
+
VALID_FIND_OPTIONS << :use_slave
|
99
|
+
|
100
|
+
# Override the standard find to check for the :use_slave option. When specified, the
|
101
|
+
# resulting query will be sent to a slave machine.
|
102
|
+
def find_every(options)
|
103
|
+
result = if options[:use_slave] && connection.is_a?(ConnectionAdapters::MysqlReplicationAdapter)
|
104
|
+
connection.load_balance_query do
|
105
|
+
old_find_every(options)
|
106
|
+
end
|
107
|
+
else
|
108
|
+
old_find_every(options)
|
109
|
+
end
|
110
|
+
result
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
|
115
|
+
alias_method :old_find_by_sql, :find_by_sql
|
116
|
+
# Override find_by_sql so that you can tell it to selectively use a slave machine
|
117
|
+
def find_by_sql(sql, use_slave = false)
|
118
|
+
if use_slave && connection.is_a?(ConnectionAdapters::MysqlReplicationAdapter)
|
119
|
+
connection.load_balance_query {old_find_by_sql sql}
|
120
|
+
else
|
121
|
+
old_find_by_sql sql
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
127
|
+
|
128
|
+
module ConnectionAdapters
|
129
|
+
class CannotWriteToSlave < Exception
|
130
|
+
end
|
131
|
+
|
132
|
+
#class AbstractAdapter
|
133
|
+
# Adding this method allows non-mysql-replication adapter applications to function without changing
|
134
|
+
# code. Useful in development and test.
|
135
|
+
#def load_balance_query
|
136
|
+
# yield
|
137
|
+
#end
|
138
|
+
#end
|
139
|
+
|
140
|
+
class MysqlColumn < Column #:nodoc:
|
141
|
+
#TYPES_ALLOWING_EMPTY_STRING_DEFAULT = Set.new([:binary, :string, :text])
|
142
|
+
|
143
|
+
def initialize(name, default, sql_type = nil, null = true)
|
144
|
+
@original_default = default
|
145
|
+
super
|
146
|
+
@default = nil if missing_default_forged_as_empty_string?
|
147
|
+
end
|
148
|
+
|
149
|
+
private
|
150
|
+
def simplified_type(field_type)
|
151
|
+
return :boolean if MysqlAdapter.emulate_booleans && field_type.downcase.index("tinyint(1)")
|
152
|
+
return :string if field_type =~ /enum/i
|
153
|
+
super
|
154
|
+
end
|
155
|
+
|
156
|
+
# MySQL misreports NOT NULL column default when none is given.
|
157
|
+
# We can't detect this for columns which may have a legitimate ''
|
158
|
+
# default (string, text, binary) but we can for others (integer,
|
159
|
+
# datetime, boolean, and the rest).
|
160
|
+
#
|
161
|
+
# Test whether the column has default '', is not null, and is not
|
162
|
+
# a type allowing default ''.
|
163
|
+
def missing_default_forged_as_empty_string?
|
164
|
+
!null && @original_default == '' && !TYPES_ALLOWING_EMPTY_STRING_DEFAULT.include?(type)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with
|
169
|
+
# the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/).
|
170
|
+
#
|
171
|
+
# Options:
|
172
|
+
#
|
173
|
+
# * <tt>:host</tt> -- Defaults to localhost
|
174
|
+
# * <tt>:port</tt> -- Defaults to 3306
|
175
|
+
# * <tt>:socket</tt> -- Defaults to /tmp/mysql.sock
|
176
|
+
# * <tt>:username</tt> -- Defaults to root
|
177
|
+
# * <tt>:password</tt> -- Defaults to nothing
|
178
|
+
# * <tt>:database</tt> -- The name of the database. No default, must be provided.
|
179
|
+
# * <tt>:sslkey</tt> -- Necessary to use MySQL with an SSL connection
|
180
|
+
# * <tt>:sslcert</tt> -- Necessary to use MySQL with an SSL connection
|
181
|
+
# * <tt>:sslcapath</tt> -- Necessary to use MySQL with an SSL connection
|
182
|
+
# * <tt>:sslcipher</tt> -- Necessary to use MySQL with an SSL connection
|
183
|
+
#
|
184
|
+
# By default, the MysqlAdapter will consider all columns of type tinyint(1)
|
185
|
+
# as boolean. If you wish to disable this emulation (which was the default
|
186
|
+
# behavior in versions 0.13.1 and earlier) you can add the following line
|
187
|
+
# to your environment.rb file:
|
188
|
+
#
|
189
|
+
# ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false
|
190
|
+
class MysqlReplicationAdapter < AbstractAdapter
|
191
|
+
@@emulate_booleans = true
|
192
|
+
cattr_accessor :emulate_booleans
|
193
|
+
@@use_master_only = false
|
194
|
+
cattr_accessor :use_master_only
|
195
|
+
|
196
|
+
LOST_CONNECTION_ERROR_MESSAGES = [
|
197
|
+
"Server shutdown in progress",
|
198
|
+
"Broken pipe",
|
199
|
+
"Lost connection to MySQL server during query",
|
200
|
+
"MySQL server has gone away"
|
201
|
+
]
|
202
|
+
|
203
|
+
def initialize(connection, logger, connection_options, config)
|
204
|
+
super(connection, logger)
|
205
|
+
@connection_options, @config = connection_options, config
|
206
|
+
|
207
|
+
connect
|
208
|
+
end
|
209
|
+
|
210
|
+
def adapter_name #:nodoc:
|
211
|
+
'MySQLReplication'
|
212
|
+
end
|
213
|
+
|
214
|
+
def supports_migrations? #:nodoc:
|
215
|
+
true
|
216
|
+
end
|
217
|
+
|
218
|
+
def native_database_types #:nodoc:
|
219
|
+
{
|
220
|
+
:primary_key => "int(11) DEFAULT NULL auto_increment PRIMARY KEY",
|
221
|
+
:string => { :name => "varchar", :limit => 255 },
|
222
|
+
:text => { :name => "text" },
|
223
|
+
:integer => { :name => "int", :limit => 11 },
|
224
|
+
:float => { :name => "float" },
|
225
|
+
:decimal => { :name => "decimal" },
|
226
|
+
:datetime => { :name => "datetime" },
|
227
|
+
:timestamp => { :name => "datetime" },
|
228
|
+
:time => { :name => "time" },
|
229
|
+
:date => { :name => "date" },
|
230
|
+
:binary => { :name => "blob" },
|
231
|
+
:boolean => { :name => "tinyint", :limit => 1 }
|
232
|
+
}
|
233
|
+
end
|
234
|
+
|
235
|
+
# the magic load_balance method
|
236
|
+
def load_balance_query
|
237
|
+
old_connection = @connection
|
238
|
+
@connection = select_clone
|
239
|
+
yield
|
240
|
+
ensure
|
241
|
+
@connection = old_connection
|
242
|
+
end
|
243
|
+
|
244
|
+
# choose a random clone to use for the moment
|
245
|
+
def select_clone
|
246
|
+
# if we happen not to be connected to any clones, just use the master
|
247
|
+
return @master if @clones.empty?
|
248
|
+
# return a random clone
|
249
|
+
return @clones[rand(@clones.size - 1)]
|
250
|
+
end
|
251
|
+
|
252
|
+
# This method raises an exception if the current connection is a clone. It is called inside
|
253
|
+
# all of the methods that typically cause database writes. This keeps the developer from
|
254
|
+
# doing any writes when inside a slave query block.
|
255
|
+
def ensure_master
|
256
|
+
raise CannotWriteToSlave, "You attempted to perform a write operation inside a slave-balanced read block." unless @connection == @master
|
257
|
+
end
|
258
|
+
|
259
|
+
# QUOTING ==================================================
|
260
|
+
|
261
|
+
def quote(value, column = nil)
|
262
|
+
if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
|
263
|
+
s = column.class.string_to_binary(value).unpack("H*")[0]
|
264
|
+
"x'#{s}'"
|
265
|
+
elsif value.kind_of?(BigDecimal)
|
266
|
+
"'#{value.to_s("F")}'"
|
267
|
+
else
|
268
|
+
super
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
def quote_column_name(name) #:nodoc:
|
273
|
+
"`#{name}`"
|
274
|
+
end
|
275
|
+
|
276
|
+
def quote_string(string) #:nodoc:
|
277
|
+
@connection.quote(string)
|
278
|
+
end
|
279
|
+
|
280
|
+
def quoted_true
|
281
|
+
"1"
|
282
|
+
end
|
283
|
+
|
284
|
+
def quoted_false
|
285
|
+
"0"
|
286
|
+
end
|
287
|
+
|
288
|
+
|
289
|
+
# CONNECTION MANAGEMENT ====================================
|
290
|
+
|
291
|
+
def active?
|
292
|
+
if @connection.respond_to?(:stat)
|
293
|
+
@connection.stat
|
294
|
+
else
|
295
|
+
@connection.query 'select 1'
|
296
|
+
end
|
297
|
+
|
298
|
+
# mysql-ruby doesn't raise an exception when stat fails.
|
299
|
+
if @connection.respond_to?(:errno)
|
300
|
+
@connection.errno.zero?
|
301
|
+
else
|
302
|
+
true
|
303
|
+
end
|
304
|
+
rescue Mysql::Error
|
305
|
+
false
|
306
|
+
end
|
307
|
+
|
308
|
+
def reconnect!
|
309
|
+
disconnect!
|
310
|
+
connect
|
311
|
+
end
|
312
|
+
|
313
|
+
def disconnect!
|
314
|
+
@connection.close rescue nil
|
315
|
+
@clones.each do |clone|
|
316
|
+
clone.close rescue nil
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
|
321
|
+
# DATABASE STATEMENTS ======================================
|
322
|
+
|
323
|
+
def execute(sql, name = nil) #:nodoc:
|
324
|
+
log(sql, "#{name} against #{@connection.host_info}") do
|
325
|
+
@connection.query(sql)
|
326
|
+
end
|
327
|
+
rescue ActiveRecord::StatementInvalid => exception
|
328
|
+
if exception.message.split(":").first =~ /Packets out of order/
|
329
|
+
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."
|
330
|
+
else
|
331
|
+
raise
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
def insert(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc:
|
336
|
+
ensure_master
|
337
|
+
execute(sql, name = nil)
|
338
|
+
id_value || @connection.insert_id
|
339
|
+
end
|
340
|
+
|
341
|
+
def update(sql, name = nil) #:nodoc:
|
342
|
+
ensure_master
|
343
|
+
execute(sql, name)
|
344
|
+
@connection.affected_rows
|
345
|
+
end
|
346
|
+
|
347
|
+
def begin_db_transaction #:nodoc:
|
348
|
+
execute "BEGIN"
|
349
|
+
rescue Exception
|
350
|
+
# Transactions aren't supported
|
351
|
+
end
|
352
|
+
|
353
|
+
def commit_db_transaction #:nodoc:
|
354
|
+
execute "COMMIT"
|
355
|
+
rescue Exception
|
356
|
+
# Transactions aren't supported
|
357
|
+
end
|
358
|
+
|
359
|
+
def rollback_db_transaction #:nodoc:
|
360
|
+
execute "ROLLBACK"
|
361
|
+
rescue Exception
|
362
|
+
# Transactions aren't supported
|
363
|
+
end
|
364
|
+
|
365
|
+
|
366
|
+
def add_limit_offset!(sql, options) #:nodoc:
|
367
|
+
if limit = options[:limit]
|
368
|
+
unless offset = options[:offset]
|
369
|
+
sql << " LIMIT #{limit}"
|
370
|
+
else
|
371
|
+
sql << " LIMIT #{offset}, #{limit}"
|
372
|
+
end
|
373
|
+
end
|
374
|
+
end
|
375
|
+
|
376
|
+
|
377
|
+
# SCHEMA STATEMENTS ========================================
|
378
|
+
|
379
|
+
def structure_dump #:nodoc:
|
380
|
+
if supports_views?
|
381
|
+
sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
|
382
|
+
else
|
383
|
+
sql = "SHOW TABLES"
|
384
|
+
end
|
385
|
+
|
386
|
+
select_all(sql).inject("") do |structure, table|
|
387
|
+
table.delete('Table_type')
|
388
|
+
structure += select_one("SHOW CREATE TABLE #{table.to_a.first.last}")["Create Table"] + ";\n\n"
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
def recreate_database(name) #:nodoc:
|
393
|
+
drop_database(name)
|
394
|
+
create_database(name)
|
395
|
+
end
|
396
|
+
|
397
|
+
def create_database(name) #:nodoc:
|
398
|
+
execute "CREATE DATABASE `#{name}`"
|
399
|
+
end
|
400
|
+
|
401
|
+
def drop_database(name) #:nodoc:
|
402
|
+
execute "DROP DATABASE IF EXISTS `#{name}`"
|
403
|
+
end
|
404
|
+
|
405
|
+
def current_database
|
406
|
+
select_one("SELECT DATABASE() as db")["db"]
|
407
|
+
end
|
408
|
+
|
409
|
+
def tables(name = nil) #:nodoc:
|
410
|
+
tables = []
|
411
|
+
execute("SHOW TABLES", name).each { |field| tables << field[0] }
|
412
|
+
tables
|
413
|
+
end
|
414
|
+
|
415
|
+
def indexes(table_name, name = nil)#:nodoc:
|
416
|
+
indexes = []
|
417
|
+
current_index = nil
|
418
|
+
execute("SHOW KEYS FROM #{table_name}", name).each do |row|
|
419
|
+
if current_index != row[2]
|
420
|
+
next if row[2] == "PRIMARY" # skip the primary key
|
421
|
+
current_index = row[2]
|
422
|
+
indexes << IndexDefinition.new(row[0], row[2], row[1] == "0", [])
|
423
|
+
end
|
424
|
+
|
425
|
+
indexes.last.columns << row[4]
|
426
|
+
end
|
427
|
+
indexes
|
428
|
+
end
|
429
|
+
|
430
|
+
def columns(table_name, name = nil)#:nodoc:
|
431
|
+
sql = "SHOW FIELDS FROM #{table_name}"
|
432
|
+
columns = []
|
433
|
+
execute(sql, name).each { |field| columns << MysqlColumn.new(field[0], field[4], field[1], field[2] == "YES") }
|
434
|
+
columns
|
435
|
+
end
|
436
|
+
|
437
|
+
def create_table(name, options = {}) #:nodoc:
|
438
|
+
super(name, {:options => "ENGINE=InnoDB"}.merge(options))
|
439
|
+
end
|
440
|
+
|
441
|
+
def rename_table(name, new_name)
|
442
|
+
execute "RENAME TABLE #{name} TO #{new_name}"
|
443
|
+
end
|
444
|
+
|
445
|
+
def change_column_default(table_name, column_name, default) #:nodoc:
|
446
|
+
current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"]
|
447
|
+
|
448
|
+
execute("ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{current_type} DEFAULT #{quote(default)}")
|
449
|
+
end
|
450
|
+
|
451
|
+
def change_column(table_name, column_name, type, options = {}) #:nodoc:
|
452
|
+
unless options_include_default?(options)
|
453
|
+
options[:default] = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Default"]
|
454
|
+
end
|
455
|
+
|
456
|
+
change_column_sql = "ALTER TABLE #{table_name} CHANGE #{column_name} #{column_name} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
|
457
|
+
add_column_options!(change_column_sql, options)
|
458
|
+
execute(change_column_sql)
|
459
|
+
end
|
460
|
+
|
461
|
+
def rename_column(table_name, column_name, new_column_name) #:nodoc:
|
462
|
+
current_type = select_one("SHOW COLUMNS FROM #{table_name} LIKE '#{column_name}'")["Type"]
|
463
|
+
execute "ALTER TABLE #{table_name} CHANGE #{column_name} #{new_column_name} #{current_type}"
|
464
|
+
end
|
465
|
+
|
466
|
+
|
467
|
+
private
|
468
|
+
def connect
|
469
|
+
encoding = @config[:encoding]
|
470
|
+
if encoding
|
471
|
+
@connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
|
472
|
+
end
|
473
|
+
@connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) if @config[:sslkey]
|
474
|
+
@connection.real_connect(*@connection_options)
|
475
|
+
execute("SET NAMES '#{encoding}'") if encoding
|
476
|
+
|
477
|
+
# By default, MySQL 'where id is null' selects the last inserted id.
|
478
|
+
# Turn this off. http://dev.rubyonrails.org/ticket/6778
|
479
|
+
execute("SET SQL_AUTO_IS_NULL=0")
|
480
|
+
|
481
|
+
# save the master in a separate instance variable so we always know what it is
|
482
|
+
@master = @connection
|
483
|
+
|
484
|
+
# connect to all the clone machines
|
485
|
+
@clones = []
|
486
|
+
if @config[:clones]
|
487
|
+
@config[:clones].each do |clone|
|
488
|
+
conn = Mysql.init
|
489
|
+
if encoding
|
490
|
+
conn.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil
|
491
|
+
end
|
492
|
+
conn.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) if @config[:sslkey]
|
493
|
+
|
494
|
+
conn.real_connect(clone["host"], clone["username"], clone["password"], clone["database"], clone["port"], clone["socket"])
|
495
|
+
@clones << conn
|
496
|
+
end
|
497
|
+
else
|
498
|
+
# warning, no slaves specified.
|
499
|
+
warn "Warning: MysqlReplicationAdapter in use, but no slave database connections specified."
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
def select(sql, name = nil)
|
504
|
+
@connection.query_with_result = true
|
505
|
+
result = execute(sql, name)
|
506
|
+
rows = result.all_hashes
|
507
|
+
result.free
|
508
|
+
rows
|
509
|
+
end
|
510
|
+
|
511
|
+
def supports_views?
|
512
|
+
version[0] >= 5
|
513
|
+
end
|
514
|
+
|
515
|
+
def version
|
516
|
+
@version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
|
517
|
+
end
|
518
|
+
end
|
519
|
+
end
|
520
|
+
end
|