mysql_mirror 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +43 -0
- data/Rakefile +20 -0
- data/VERSION +1 -0
- data/lib/mysql_mirror.rb +190 -0
- metadata +66 -0
data/README.rdoc
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
= Mysql Mirror
|
2
|
+
Use MysqlMirror to mirror data between databases. This can be useful when you want to update your
|
3
|
+
development or staging environments with real data to work with. Or, if you do some heavy lifting
|
4
|
+
calculations on another server, you might want to use a seperate database on another host, etc.
|
5
|
+
|
6
|
+
=== General Approach
|
7
|
+
- Mirror Across Hosts: performs a mysql_dump to an sql file, then imports file to target host
|
8
|
+
- Mirror Same Host: uses CREATE TABLE ( SELECT ... ) style for mirroring. Much faster than mysql_dump
|
9
|
+
|
10
|
+
Note:
|
11
|
+
ALL information will be lost in the tables mirrored to the Target Database
|
12
|
+
|
13
|
+
== Dependencies
|
14
|
+
- Active Record
|
15
|
+
- FileUtils
|
16
|
+
|
17
|
+
== Usage
|
18
|
+
Basic usage, copy production db to development
|
19
|
+
@m = MysqlMirror.new({
|
20
|
+
:source => :production,
|
21
|
+
:target => :development
|
22
|
+
})
|
23
|
+
|
24
|
+
Choose what tables you want to bring over and how you want to scope them...
|
25
|
+
@m = MysqlMirror.new({
|
26
|
+
:source => :production,
|
27
|
+
:target => :development,
|
28
|
+
:tables => [:users, :widgets],
|
29
|
+
:where => {:users => "is_admin NOT NULL"},
|
30
|
+
})
|
31
|
+
|
32
|
+
Database information not in your database.yml file? (Or Not Running Rails?) No Problem!
|
33
|
+
@m = MysqlMirror.new({
|
34
|
+
:source => { :database => "app_production", :user => ..., :password => ..., :hostname => ...},
|
35
|
+
:target => {:database => "app_development", :hostname => 'localhost'}
|
36
|
+
})
|
37
|
+
|
38
|
+
Want to use everything in :production environment (user, pass, host) but need to change the database?
|
39
|
+
@m = MysqlMirror.new({
|
40
|
+
:source => :production,
|
41
|
+
:override => {:source => {:database => "heavy_calculations_database"}},
|
42
|
+
:target => :production
|
43
|
+
})
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gemspec|
|
7
|
+
gemspec.name = "mysql_mirror"
|
8
|
+
gemspec.summary = "Helps mirror MySql Databases"
|
9
|
+
gemspec.description = "Will mirror tables / databases between mysql databases and across hosts"
|
10
|
+
gemspec.email = "peterleonhardt@gmail.com"
|
11
|
+
gemspec.homepage = "http://github.com/pjleonhardt/mysql_mirror"
|
12
|
+
gemspec.authors = ["Peter Leonhardt", "Joe Goggins"]
|
13
|
+
gemspec.files = FileList["[A-Z]*", "lib/mysql_mirror.rb"]
|
14
|
+
end
|
15
|
+
Jeweler::GemcutterTasks.new
|
16
|
+
rescue LoadError
|
17
|
+
puts "Jeweler not available. Please install the jeweler gem."
|
18
|
+
end
|
19
|
+
|
20
|
+
Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.1
|
data/lib/mysql_mirror.rb
ADDED
@@ -0,0 +1,190 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'fileutils'
|
3
|
+
|
4
|
+
class MysqlMirror
|
5
|
+
class MysqlMirrorException < Exception; end
|
6
|
+
|
7
|
+
class Source < ActiveRecord::Base
|
8
|
+
end
|
9
|
+
|
10
|
+
class Target < ActiveRecord::Base
|
11
|
+
end
|
12
|
+
|
13
|
+
attr_accessor :tables, :where
|
14
|
+
|
15
|
+
def initialize(options = {})
|
16
|
+
unless ([:source, :target] - options.keys).blank?
|
17
|
+
# Need to specify a Source and Target database
|
18
|
+
raise MysqlMirrorException.new("You must specify both Source and Target connections")
|
19
|
+
end
|
20
|
+
|
21
|
+
self.tables = options.delete(:tables)
|
22
|
+
self.where = options.delete(:where)
|
23
|
+
|
24
|
+
overrides = options.delete(:override) || {}
|
25
|
+
source_override = overrides.delete(:source) || {}
|
26
|
+
target_override = overrides.delete(:target) || {}
|
27
|
+
|
28
|
+
@source_config = get_configuration(options.delete(:source))
|
29
|
+
@target_config = get_configuration(options.delete(:target))
|
30
|
+
|
31
|
+
@source_config.merge!(source_override)
|
32
|
+
@target_config.merge!(target_override)
|
33
|
+
|
34
|
+
# @commands is an array of methods to call
|
35
|
+
if mirroring_same_host?
|
36
|
+
@commands = commands_for_local_mirror
|
37
|
+
else
|
38
|
+
@commands = commands_for_remote_mirror
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def commands_for_local_mirror
|
43
|
+
[:local_copy]
|
44
|
+
end
|
45
|
+
|
46
|
+
def commands_for_remote_mirror
|
47
|
+
[
|
48
|
+
:remote_mysqldump,
|
49
|
+
:remote_tmp_file_table_rename,
|
50
|
+
:remote_insert_command,
|
51
|
+
:remote_rename_tmp_tables,
|
52
|
+
:remote_remove_tmp_file
|
53
|
+
]
|
54
|
+
end
|
55
|
+
|
56
|
+
def mirroring_same_host?
|
57
|
+
@source_config[:host] == @target_config[:host]
|
58
|
+
end
|
59
|
+
|
60
|
+
def execute!
|
61
|
+
@start_time = Time.now
|
62
|
+
@source = connect_to(:source)
|
63
|
+
@target = connect_to(:target)
|
64
|
+
|
65
|
+
@commands.each do |c|
|
66
|
+
self.send(c)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
|
71
|
+
private
|
72
|
+
# e.g, connect_to(:source)
|
73
|
+
# => MysqlMirror::Source.establish_connection(@source_config).connection
|
74
|
+
#
|
75
|
+
def connect_to(which)
|
76
|
+
"MysqlMirror::#{which.to_s.classify}".constantize.establish_connection(self.instance_variable_get("@#{which}_config")).connection
|
77
|
+
end
|
78
|
+
|
79
|
+
def local_copy
|
80
|
+
get_tables.each do |table|
|
81
|
+
target_db = @target_config[:database]
|
82
|
+
source_db = @source_config[:database]
|
83
|
+
target_table = "#{target_db}.#{table}"
|
84
|
+
target_tmp_table = "#{target_db}.#{table}_MirrorTmp"
|
85
|
+
target_old_table = "#{target_db}.#{table}_OldMarkedToDelete"
|
86
|
+
source_table = "#{source_db}.#{table}"
|
87
|
+
|
88
|
+
|
89
|
+
prime_statement_1 = "DROP TABLE IF EXISTS #{target_tmp_table}"
|
90
|
+
prime_statement_2 = "CREATE TABLE IF NOT EXISTS #{target_table} LIKE #{source_table}"
|
91
|
+
|
92
|
+
create_statement = "CREATE TABLE #{target_tmp_table} LIKE #{source_table}"
|
93
|
+
|
94
|
+
select_clause = "SELECT * FROM #{source_table}"
|
95
|
+
select_clause << " WHERE #{self.where[table]}" unless (self.where.blank? or self.where[table].blank?)
|
96
|
+
|
97
|
+
insert_statement = "INSERT INTO #{target_tmp_table} #{select_clause}"
|
98
|
+
rename_statement = "RENAME TABLE #{target_table} TO #{target_old_table}, #{target_tmp_table} TO #{target_table}"
|
99
|
+
cleanup_statement = "DROP TABLE IF EXISTS #{target_old_table}"
|
100
|
+
|
101
|
+
staments_to_run = [prime_statement_1, prime_statement_2, create_statement, insert_statement, rename_statement, cleanup_statement]
|
102
|
+
|
103
|
+
staments_to_run.each do |statement|
|
104
|
+
@target.execute(statement)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def mysqldump_command_prefix
|
110
|
+
"mysqldump --compact=TRUE --max_allowed_packet=100663296 --extended-insert=TRUE --lock-tables=FALSE --add-locks=FALSE --add-drop-table=FALSE"
|
111
|
+
end
|
112
|
+
|
113
|
+
def remote_mysqldump
|
114
|
+
@tmp_file_name = "mysql_mirror_#{@start_time.to_i}.sql"
|
115
|
+
tables = get_tables.map(&:to_s).join(" ")
|
116
|
+
where = self.where.blank? ? "" : "--where\"#{@source_config[:where]}\""
|
117
|
+
config = "-u#{@source_config[:username]} -p'#{@source_config[:password]}' -h #{@source_config[:host]} #{@source_config[:database]}"
|
118
|
+
|
119
|
+
the_cmd = "#{mysqldump_command_prefix} #{where} #{config} #{tables} > #{@tmp_file_name}"
|
120
|
+
puts the_cmd
|
121
|
+
`#{the_cmd}`
|
122
|
+
end
|
123
|
+
|
124
|
+
def remote_tmp_file_table_rename
|
125
|
+
create_or_insert_regex = Regexp.new('(^CREATE TABLE|^INSERT INTO)( `)(.+?)(`)(.+)')
|
126
|
+
new_file_name = @tmp_file_name + ".replaced.sql"
|
127
|
+
|
128
|
+
new_file = File.new(new_file_name, "w")
|
129
|
+
IO.foreach(@tmp_file_name) do |line|
|
130
|
+
if match_data = line.match(create_or_insert_regex)
|
131
|
+
table_name = match_data[3]
|
132
|
+
new_table_name = "#{table_name}_#{@start_time.to_i}"
|
133
|
+
new_file.puts match_data[1] + match_data[2] + new_table_name + match_data[4]+ match_data[5]
|
134
|
+
else
|
135
|
+
new_file.puts line
|
136
|
+
end
|
137
|
+
end
|
138
|
+
new_file.close
|
139
|
+
# replace dump'd sql file with this gsub'd one
|
140
|
+
FileUtils.move(new_file_name, @tmp_file_name)
|
141
|
+
end
|
142
|
+
|
143
|
+
def remote_insert_command
|
144
|
+
config = "-u#{@target_config[:username]} -p'#{@target_config[:password]}' -h #{@target_config[:host]} #{@target_config[:database]}"
|
145
|
+
the_cmd = "mysql #{config} < #{@tmp_file_name}"
|
146
|
+
`#{the_cmd}`
|
147
|
+
end
|
148
|
+
|
149
|
+
def remote_rename_tmp_tables
|
150
|
+
get_tables.each do |table|
|
151
|
+
tmp_table_name = "#{table}_#{@start_time.to_i}"
|
152
|
+
old_table_name = "#{table}_OldMarkedToDelete"
|
153
|
+
|
154
|
+
@target.transaction do
|
155
|
+
@target.execute("DROP TABLE IF EXISTS #{old_table_name}")
|
156
|
+
@target.execute("RENAME TABLE #{table} TO #{old_table_name}, #{tmp_table_name} TO #{table}")
|
157
|
+
@target.execute("DROP TABLE IF EXISTS #{old_table_name}")
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def remote_remove_tmp_file
|
163
|
+
FileUtils.rm(@tmp_file_name)
|
164
|
+
end
|
165
|
+
|
166
|
+
def get_tables
|
167
|
+
the_tables = self.tables.blank? ? @source.select_values("SHOW TABLES").map!(&:to_sym) : self.tables
|
168
|
+
end
|
169
|
+
|
170
|
+
|
171
|
+
def get_configuration(env_or_hash)
|
172
|
+
config = env_or_hash
|
173
|
+
|
174
|
+
if(env_or_hash.is_a? Symbol)
|
175
|
+
config = ActiveRecord::Base.configurations[env_or_hash.to_s]
|
176
|
+
end
|
177
|
+
|
178
|
+
config.symbolize_keys
|
179
|
+
end
|
180
|
+
|
181
|
+
end
|
182
|
+
|
183
|
+
|
184
|
+
|
185
|
+
|
186
|
+
|
187
|
+
|
188
|
+
|
189
|
+
|
190
|
+
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mysql_mirror
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 1
|
8
|
+
- 1
|
9
|
+
version: 0.1.1
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Peter Leonhardt
|
13
|
+
- Joe Goggins
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2010-03-26 00:00:00 -05:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: Will mirror tables / databases between mysql databases and across hosts
|
23
|
+
email: peterleonhardt@gmail.com
|
24
|
+
executables: []
|
25
|
+
|
26
|
+
extensions: []
|
27
|
+
|
28
|
+
extra_rdoc_files:
|
29
|
+
- README.rdoc
|
30
|
+
files:
|
31
|
+
- README.rdoc
|
32
|
+
- Rakefile
|
33
|
+
- VERSION
|
34
|
+
- lib/mysql_mirror.rb
|
35
|
+
has_rdoc: true
|
36
|
+
homepage: http://github.com/pjleonhardt/mysql_mirror
|
37
|
+
licenses: []
|
38
|
+
|
39
|
+
post_install_message:
|
40
|
+
rdoc_options:
|
41
|
+
- --charset=UTF-8
|
42
|
+
require_paths:
|
43
|
+
- lib
|
44
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
45
|
+
requirements:
|
46
|
+
- - ">="
|
47
|
+
- !ruby/object:Gem::Version
|
48
|
+
segments:
|
49
|
+
- 0
|
50
|
+
version: "0"
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
segments:
|
56
|
+
- 0
|
57
|
+
version: "0"
|
58
|
+
requirements: []
|
59
|
+
|
60
|
+
rubyforge_project:
|
61
|
+
rubygems_version: 1.3.6
|
62
|
+
signing_key:
|
63
|
+
specification_version: 3
|
64
|
+
summary: Helps mirror MySql Databases
|
65
|
+
test_files: []
|
66
|
+
|