mysql_mirror 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|
+
|