table_syncer 0.2.0 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +6 -0
- data/lib/table_syncer/version.rb +1 -1
- data/lib/table_syncer.rb +92 -49
- data/website/index.html +1 -1
- metadata +7 -6
data/History.txt
CHANGED
@@ -1,3 +1,9 @@
|
|
1
|
+
==0.3.0 2008-10-15
|
2
|
+
|
3
|
+
* Now uses the more conservative [and slower] query_with_result = true, instead of the faster and potentially scary query_with_result = false -- see http://betterlogic.com/roger/?p=473
|
4
|
+
* Now has a working --extra_sql=SQL
|
5
|
+
* Recommended upgrade.
|
6
|
+
|
1
7
|
== 0.2.0 2009-09-39
|
2
8
|
|
3
9
|
* Now prompts you if you use --commit [so you don't do it on accident]
|
data/lib/table_syncer/version.rb
CHANGED
data/lib/table_syncer.rb
CHANGED
@@ -13,36 +13,50 @@
|
|
13
13
|
# If it doesn't, then run the command output [it will print out the ssh tunnel command appropriate] in a different terminal window
|
14
14
|
|
15
15
|
# 2008 Roger Pack Public Domain
|
16
|
-
# No warranty of any type :)
|
16
|
+
# No warranty expressed or implied of any type :)
|
17
17
|
|
18
18
|
require 'rubygems'
|
19
19
|
begin
|
20
|
-
require 'mysqlplus'
|
20
|
+
require 'mysqlplus' # it's barely faster
|
21
21
|
rescue LoadError
|
22
22
|
require "mysql"
|
23
23
|
end
|
24
24
|
require 'optparse'
|
25
25
|
|
26
26
|
# define some databases and how you connect with them, if foreign
|
27
|
-
|
28
|
-
|
27
|
+
local_db = {:host => '127.0.0.1', :user => 'root', :password => '', :db => 'local_leadgen_dev'}
|
28
|
+
ties_db = {:user => 'db_user_name_ex_root', :password => 'mysql_password_for_that_user', :db => 'wilkboar_ties', :ssh_host => 'my_host.com', :ssh_user => 'ssh_login'} # ssh example -- doesn't take passwords here but attempts to create an ssh tunnel for you to that host.
|
29
29
|
|
30
|
-
|
30
|
+
# I suppose you wouldn't need an ssh_user if you had established the tunnel youself [ex: if you had already created a tunnel on port 9000 to the remote Mysql port]
|
31
|
+
ties_db_no_host_even = {:user => 'db_user_name_ex_root', :password => 'mysql_password_for_that_user', :db => 'wilkboar_ties', :tunnel_local_port_to_use => 9000}
|
32
|
+
# if you have multiple remote hosts then you'll want to assign a different port for each host
|
31
33
|
|
32
|
-
|
33
|
-
|
34
|
-
|
34
|
+
ties_super_advanced = {:user => 'mysql_user', :password => 'mysql_pass_for_that_user', :ssh_host => 'helpme.com', :ssh_port => 888, :ssh_user => 'ssh_login_name', :ssh_local_to_host => '127.0.0.1 -- change if you want it to connect to a different host "from the remote"', :ssh_local_to_port => '4000 instead of the normal 3306'}
|
35
|
+
# ssh_port is the ssh port [instead of 22] for the remote host
|
36
|
+
|
37
|
+
all_database_names = ['local_db', 'ties_db', 'ties_db_no_host_even', 'ties_super_advanced'] # only used --help command is more useful and can print out the database connection names available -- not necessary
|
38
|
+
|
39
|
+
|
40
|
+
|
41
|
+
|
42
|
+
# setup defaults -- it will use these databases by default unless you specify otherwise on the command line [or use nil for none]
|
43
|
+
db_from_name = 'local_db' # replace with yours
|
44
|
+
db_to_name = 'local_db'
|
35
45
|
actually_run_queries = false # default to just previewing -- use --commit to actually run it
|
36
46
|
verbose = true
|
47
|
+
default_tables_to_sync = nil # replace with default tables to sync, like ['users']
|
37
48
|
|
38
49
|
|
39
|
-
# now parse incoming options
|
40
50
|
my_options = {}
|
41
51
|
auto_create_ssh_tunnels = true
|
42
|
-
tables_to_sync = nil
|
52
|
+
tables_to_sync = nil # leave as nil--this will be propagated by yours, above, or those passed from the command line
|
43
53
|
# make sure use normal style options, or nothing :)
|
44
54
|
my_options[:skip_the_warning_prompt_for_commit] = false
|
45
55
|
|
56
|
+
# note that ctrl-c doesn't work too well in the middle of this script, but ctrl-z + kill PID does
|
57
|
+
|
58
|
+
# now parse incoming options
|
59
|
+
|
46
60
|
do_structure_sync_only = false
|
47
61
|
|
48
62
|
OptionParser.new do |opts|
|
@@ -52,10 +66,14 @@
|
|
52
66
|
db_from_name = from_name
|
53
67
|
end
|
54
68
|
|
55
|
-
opts.on("-t", "--to=TO", "to database name #{all_database_names.inspect} \n\t\t\t\t\t\tedit databases at #{__FILE__}") do |to_name|
|
69
|
+
opts.on("-t", "--to=TO", "to database name #{all_database_names.inspect} \n\t\t\t\t\t\tedit databases descriptions at #{__FILE__}") do |to_name|
|
56
70
|
db_to_name = to_name
|
57
71
|
end
|
58
72
|
|
73
|
+
opts.on('--verbose') do
|
74
|
+
my_options[:verbose] = true
|
75
|
+
end
|
76
|
+
|
59
77
|
opts.on('-y', 'skip the warning prompt for commit') do
|
60
78
|
my_options[:skip_the_warning_prompt_for_commit] = true
|
61
79
|
end
|
@@ -68,12 +86,10 @@
|
|
68
86
|
end
|
69
87
|
|
70
88
|
opts.on("", "--commit", " tell us whether or not to actually run queries -- the default is to not run queries") do
|
71
|
-
print "DOING IT COMMITTING LIVE QUERIES\n"
|
72
89
|
actually_run_queries = true
|
73
90
|
end
|
74
91
|
|
75
92
|
opts.on("-z", "--extra_sql=STRING", "specify an sql sql string to run after the script ends [in the 'to' database]") do |sql|
|
76
|
-
print "extra sql", sql
|
77
93
|
my_options[:extra_sql] = sql
|
78
94
|
end
|
79
95
|
|
@@ -81,7 +97,7 @@
|
|
81
97
|
verbose = false
|
82
98
|
end
|
83
99
|
|
84
|
-
opts.on('--perform_structure_sync_only', "Do a structure morph for the designated tables --
|
100
|
+
opts.on('--perform_structure_sync_only', "Do a structure morph for the designated tables -- Use it with --tables=STRUCTURE_SYNC_THE_WHOLE_THING to drop any existing tables not found in the origin database and structure sync over any existing tables -- see the docs http://code.google.com/p/ruby-roger-useful-functions/wiki/TableSyncer") do
|
85
101
|
do_structure_sync_only = true
|
86
102
|
end
|
87
103
|
|
@@ -93,11 +109,14 @@
|
|
93
109
|
eval("db_from_info = #{db_from_name}")
|
94
110
|
print "to db: #{db_to_name}\n"
|
95
111
|
eval("db_to_info = #{db_to_name}")
|
96
|
-
|
97
|
-
# some error checking
|
98
|
-
raise if defined?(production) and db_to_name == 'production' and actually_run_queries # I never wanted to commit to one db
|
99
112
|
raise 'missing a database selected?' unless db_to_info and db_from_info
|
100
113
|
|
114
|
+
# skip the necessity for -y if a db is set to :expendable
|
115
|
+
my_options[:skip_the_warning_prompt_for_commit] = true if db_to_info[:expendable]
|
116
|
+
|
117
|
+
# raise if they attempt to commit to a db which has :read_only => true set in its options
|
118
|
+
raise 'attempting to commit to a read only db ' + db_to_name if db_to_info[:read_only] and actually_run_queries
|
119
|
+
|
101
120
|
# custom parse table names they have within the parameters
|
102
121
|
unless tables_to_sync
|
103
122
|
extra_table_args = ARGV.select{|arg| arg[0..0] != '-'}
|
@@ -106,7 +125,9 @@
|
|
106
125
|
for arg in extra_table_args
|
107
126
|
tables_to_sync += Array(arg.split(','))
|
108
127
|
end
|
109
|
-
|
128
|
+
elsif default_tables_to_sync
|
129
|
+
tables_to_sync = default_tables_to_sync
|
130
|
+
else
|
110
131
|
print 'no tables specified! run with --help', "\n"
|
111
132
|
exit
|
112
133
|
end
|
@@ -150,19 +171,19 @@ class Hash
|
|
150
171
|
end
|
151
172
|
end
|
152
173
|
|
153
|
-
def sync_structure(db_to, db_from, table, actually_run_queries)
|
174
|
+
def sync_structure(db_to, db_from, table, actually_run_queries, my_options)
|
154
175
|
print "structure syncing #{table}\n"
|
155
|
-
good_structure = db_from.query("desc #{table}")
|
176
|
+
good_structure = db_from.query("desc #{table}")
|
156
177
|
all_from_columns = {}
|
157
178
|
good_structure.each_hash{|h| all_from_columns[h['Field']] = h }
|
158
179
|
good_structure.free
|
159
180
|
# we basically cheat and just fakely recreate mismatched columns by "modifying them" to match the creation script given by 'show create table x' for that column
|
160
|
-
good_creation_query = db_from.query("show create table #{table}")
|
181
|
+
good_creation_query = db_from.query("show create table #{table}")
|
161
182
|
create_whole_table_script = good_creation_query.fetch_hash['Create Table']
|
162
183
|
good_creation_script = create_whole_table_script.split("\n")
|
163
184
|
good_creation_query.free
|
164
185
|
|
165
|
-
questionable_to_structure = db_to.query("desc #{table}")
|
186
|
+
questionable_to_structure = db_to.query("desc #{table}") rescue nil
|
166
187
|
unless questionable_to_structure
|
167
188
|
if actually_run_queries
|
168
189
|
db_to.query(create_whole_table_script)
|
@@ -170,7 +191,7 @@ end
|
|
170
191
|
print "would have created new table #{table} thus: #{create_whole_table_script}\n"
|
171
192
|
db_to = db_from # fake it that they match so we don't raise any errors for the duration of this method call
|
172
193
|
end
|
173
|
-
questionable_to_structure = db_to.query("desc #{table}")
|
194
|
+
questionable_to_structure = db_to.query("desc #{table}")
|
174
195
|
end
|
175
196
|
|
176
197
|
all_to_columns = {}
|
@@ -222,25 +243,36 @@ end
|
|
222
243
|
db_to.query("ALTER TABLE #{table} DROP #{column_name}") if actually_run_queries
|
223
244
|
end
|
224
245
|
|
225
|
-
indices = db_from.query("show index from #{table};")
|
246
|
+
indices = db_from.query("show index from #{table};")
|
226
247
|
all_indices = []
|
227
248
|
indices.each_hash{|h| h.delete('Cardinality'); all_indices << h } # Cardinality doesn't make a difference...AFAIK
|
228
249
|
indices.free
|
229
250
|
|
230
|
-
existing_indices = db_to.query("show index from #{table}")
|
251
|
+
existing_indices = db_to.query("show index from #{table}")
|
231
252
|
all_existing_indices = []
|
232
253
|
existing_indices.each_hash{|h| h.delete('Cardinality'); all_existing_indices << h }
|
233
254
|
existing_indices.free
|
255
|
+
different = []
|
256
|
+
all_different = all_indices - all_existing_indices #.each{|hash| different << hash['Column_name'] unless existing_indices.include?(hash) }
|
234
257
|
apparently_lacking = all_indices.map{|index| index['Column_name']} - all_existing_indices.map{|index| index['Column_name']}
|
235
258
|
|
259
|
+
|
260
|
+
|
236
261
|
for index in apparently_lacking
|
237
262
|
# ltodo if it looks nice and generic then go ahead and add it
|
238
263
|
end
|
239
264
|
|
265
|
+
|
266
|
+
|
240
267
|
if all_indices != all_existing_indices # this is right
|
241
|
-
print "\n\nWARNING #{table}: you are missing some indexes now or there is some type of discrepancy [
|
242
|
-
|
243
|
-
|
268
|
+
print "\n\nWARNING #{table}: you are missing some indexes now or there is some type of discrepancy [indices aren't handled yet]-- you may want to add them a la\nCREATE INDEX some_name_usually_column_name_here ON #{table} (column_name_here)\n for apparently at least the following missing indices: #{apparently_lacking.inspect}
|
269
|
+
|
270
|
+
you have apparently mismatched indices for: #{all_different.map{|h| h['Column_name']}.inspect}\n\n
|
271
|
+
--you might get away with dropping the old table and letting it be recreated -- that might add the right indices -- run with --verbose to see more info"
|
272
|
+
|
273
|
+
if my_options[:verbose]
|
274
|
+
print "the 'good' one is #{all_indices.inspect}, yours is #{all_existing_indices.inspect}"
|
275
|
+
end
|
244
276
|
end
|
245
277
|
end
|
246
278
|
|
@@ -262,7 +294,7 @@ end
|
|
262
294
|
# ltodo add in the local_to_stuff here
|
263
295
|
|
264
296
|
if actually_run_queries and !my_options[:skip_the_warning_prompt_for_commit]
|
265
|
-
print "Continue (
|
297
|
+
print "Continue (y/n)?"
|
266
298
|
input = gets
|
267
299
|
if !['y', 'yes'].include? input.downcase.strip
|
268
300
|
print "aborting -- you gave me #{input}"
|
@@ -286,19 +318,25 @@ end
|
|
286
318
|
puts "This may mean a tunnel is not working" if e.error.include?('127.0.0.1')
|
287
319
|
# note that, if you do add ssh -> ssh, you may still only need one connection!
|
288
320
|
if db_from_info[:ssh_host] or db_to_info[:ssh_host]
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
321
|
+
|
322
|
+
raise 'two ssh hosts not yet a working feature--request it' if db_from_info[:ssh_host] and db_to_info[:ssh_host] and (db_from_info[:ssh_host] != db_to_info[:ssh_host])
|
323
|
+
ssh_requiring_connection = db_to_info[:ssh_host] ? db_to_info : db_from_info
|
324
|
+
ssh_port = ssh_requiring_connection[:ssh_port]
|
325
|
+
ssh_local_to_port = ssh_requiring_connection[:ssh_local_to_port] || 3306
|
326
|
+
ssh_user = ssh_requiring_connection[:ssh_user]
|
327
|
+
ssh_local_to_host = ssh_requiring_connection[:ssh_local_to_host] || 'localhost'
|
328
|
+
ssh_host = ssh_requiring_connection[:ssh_host]
|
329
|
+
|
330
|
+
local_port_to_use = ssh_requiring_connection[:tunnel_local_port_to_use] || 4000
|
331
|
+
|
332
|
+
command = "ssh -N #{ssh_port ? '-p ' + ssh_port.to_s : nil} -L #{local_port_to_use}:#{ssh_local_to_host}:#{ssh_local_to_port} #{ssh_user}@#{ssh_host} \n" # note that ssh_local_to_port is 'local on the foreign server'
|
295
333
|
if auto_create_ssh_tunnels and !retried
|
296
334
|
print "trying to auto create ssh tunnel via: #{command}\n"
|
297
335
|
Thread.new { system(command) }
|
298
336
|
retried = true # this doesn't actually work :P
|
299
337
|
retry
|
300
338
|
else
|
301
|
-
print "unable to connect to server--try running\n#{command}in another window or try again
|
339
|
+
print "unable to connect to server--try running\n\t#{command}in another window or try again!\n"
|
302
340
|
end
|
303
341
|
end
|
304
342
|
exit
|
@@ -308,19 +346,15 @@ end
|
|
308
346
|
|
309
347
|
summary_information = '' # so we can print it all (again), at the end
|
310
348
|
|
311
|
-
db_to.query_with_result = false
|
312
|
-
db_from.query_with_result = false # allow us to read them from the wire when they are coming in. Save a little CPU time :)
|
313
|
-
|
314
|
-
|
315
349
|
# we need to delete any extra tables if they're there in one and not the other
|
316
350
|
if do_structure_sync_only and tables_to_sync == ['STRUCTURE_SYNC_THE_WHOLE_THING'] or tables_to_sync == ['ALL_TABLES']
|
317
|
-
tables_from = db_from.query("show tables")
|
351
|
+
tables_from = db_from.query("show tables")
|
318
352
|
tables_from_array = []
|
319
353
|
tables_from.each_hash {|h| h.each{|k, v| tables_from_array << v}}
|
320
354
|
tables_from.free
|
321
355
|
tables_to_sync = tables_from_array
|
322
356
|
if tables_to_sync == ['STRUCTURE_SYNC_THE_WHOLE_THING'] # then we want to drop some tables if they exist
|
323
|
-
tables_to = db_to.query("show tables")
|
357
|
+
tables_to = db_to.query("show tables")
|
324
358
|
tables_to_array = []
|
325
359
|
tables_to.each_hash {|h| h.each{|k, v| tables_to_array << v}}
|
326
360
|
tables_to.free
|
@@ -336,13 +370,13 @@ end
|
|
336
370
|
for table in tables_to_sync do
|
337
371
|
print "start #{commit_style} table #{table}" + "**" * 10 + "\n"
|
338
372
|
if do_structure_sync_only
|
339
|
-
sync_structure(db_to, db_from, table, actually_run_queries)
|
373
|
+
sync_structure(db_to, db_from, table, actually_run_queries, my_options)
|
340
374
|
next
|
341
375
|
end
|
342
376
|
|
343
377
|
all_to_keys_not_yet_processed = {}
|
344
378
|
select_all_to = db_to.query("SELECT * FROM #{table}") # could easily be 'select id', as well note this assumes distinct id's! Otherwise we'd need hashes, one at a time, etc. etc.
|
345
|
-
select_all_to = select_all_to
|
379
|
+
select_all_to = select_all_to
|
346
380
|
select_all_to.each_hash { |to_element|
|
347
381
|
if all_to_keys_not_yet_processed[to_element['id']] # duplicated id's are a fringe case and not yet handled! TODO use hashes or somefin' bet-uh
|
348
382
|
raise "\n\n\n\nERROR detected a duplicated id (or the lack of id at all) in #{table} -- aborting [consider clearing [DELETE FROM #{table} in the 'to' database and trying again, if in a pinch]!\n\n\n\n"
|
@@ -351,7 +385,7 @@ end
|
|
351
385
|
}
|
352
386
|
|
353
387
|
res = db_from.query("SELECT * from #{table}")
|
354
|
-
res = res
|
388
|
+
res = res
|
355
389
|
count_updated = 0
|
356
390
|
count_created = 0
|
357
391
|
|
@@ -398,7 +432,7 @@ end
|
|
398
432
|
end
|
399
433
|
double_check_all_query = "select * from #{table} where id IN (#{ids.join(',')})" # this allows us to make sure we don't delete any doubled ones (which would be a weird situation and too odd to handle), and also so we can have a nice verbose 'we are deleting this row' message
|
400
434
|
double_check_result = db_to.query(double_check_all_query)
|
401
|
-
double_check_result = double_check_result
|
435
|
+
double_check_result = double_check_result
|
402
436
|
|
403
437
|
|
404
438
|
victims = {}
|
@@ -425,9 +459,16 @@ end
|
|
425
459
|
end
|
426
460
|
if my_options[:extra_sql] and actually_run_queries
|
427
461
|
print "doing sql #{my_options[:extra_sql]}\n"
|
428
|
-
result = db_to.query my_options[:extra_sql]
|
429
|
-
|
430
|
-
|
462
|
+
result = db_to.query( my_options[:extra_sql])
|
463
|
+
# result will only be a result set if it was a select query
|
464
|
+
|
465
|
+
if result
|
466
|
+
require 'pp'
|
467
|
+
while row = result.fetch_row do
|
468
|
+
pp row
|
469
|
+
end
|
470
|
+
result.free
|
471
|
+
end
|
431
472
|
end
|
432
473
|
db_from.close if db_from
|
433
474
|
db_to.close if db_to
|
@@ -506,3 +547,5 @@ end
|
|
506
547
|
# ltodo: alias for table names :) czs, ps, etc :)
|
507
548
|
|
508
549
|
# we really need to be able to handle many to many: just use a hash + counter based system instead of an id based system
|
550
|
+
# ltodo: use mysql table checksum :)
|
551
|
+
# ltodo: lacks using port besides 3306 locally
|
data/website/index.html
CHANGED
@@ -33,7 +33,7 @@
|
|
33
33
|
<h1>table_syncer</h1>
|
34
34
|
<div id="version" class="clickable" onclick='document.location = "http://rubyforge.org/projects/table_syncer"; return false'>
|
35
35
|
<p>Get Version</p>
|
36
|
-
<a href="http://rubyforge.org/projects/table_syncer" class="numbers">0.
|
36
|
+
<a href="http://rubyforge.org/projects/table_syncer" class="numbers">0.3.0</a>
|
37
37
|
</div>
|
38
38
|
<h1>&#x2192; ‘table_syncer’</h1>
|
39
39
|
<h2>What</h2>
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: table_syncer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Roger Pack
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2008-
|
12
|
+
date: 2008-11-22 00:00:00 -07:00
|
13
13
|
default_executable:
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
@@ -20,7 +20,7 @@ dependencies:
|
|
20
20
|
requirements:
|
21
21
|
- - ">="
|
22
22
|
- !ruby/object:Gem::Version
|
23
|
-
version: 1.
|
23
|
+
version: 1.8.0
|
24
24
|
version:
|
25
25
|
description: tool to synchronize data across databases
|
26
26
|
email:
|
@@ -33,6 +33,7 @@ extra_rdoc_files:
|
|
33
33
|
- History.txt
|
34
34
|
- Manifest.txt
|
35
35
|
- PostInstall.txt
|
36
|
+
- README.rdoc
|
36
37
|
- website/index.txt
|
37
38
|
files:
|
38
39
|
- History.txt
|
@@ -72,7 +73,7 @@ post_install_message: |+
|
|
72
73
|
|
73
74
|
rdoc_options:
|
74
75
|
- --main
|
75
|
-
- README.
|
76
|
+
- README.rdoc
|
76
77
|
require_paths:
|
77
78
|
- lib
|
78
79
|
required_ruby_version: !ruby/object:Gem::Requirement
|
@@ -90,10 +91,10 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
90
91
|
requirements: []
|
91
92
|
|
92
93
|
rubyforge_project: table-syncer
|
93
|
-
rubygems_version: 1.3.
|
94
|
+
rubygems_version: 1.3.1
|
94
95
|
signing_key:
|
95
96
|
specification_version: 2
|
96
97
|
summary: tool to synchronize data across databases
|
97
98
|
test_files:
|
98
|
-
- test/test_helper.rb
|
99
99
|
- test/test_table_syncer.rb
|
100
|
+
- test/test_helper.rb
|