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.
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
+