mysql_mirror 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (5) hide show
  1. data/README.rdoc +43 -0
  2. data/Rakefile +20 -0
  3. data/VERSION +1 -0
  4. data/lib/mysql_mirror.rb +190 -0
  5. 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
@@ -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
+