table_syncer 0.2.0 → 0.3.0

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/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]
@@ -1,7 +1,7 @@
1
1
  module TableSyncer
2
2
  module VERSION #:nodoc:
3
3
  MAJOR = 0
4
- MINOR = 2
4
+ MINOR = 3
5
5
  TINY = 0
6
6
 
7
7
  STRING = [MAJOR, MINOR, TINY].join('.')
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
- example_db_1 = {:host => '127.0.0.1', :user => 'root', :password => '', :db => 'wilkboar_ties'}
28
- example_db_2 = {:host => '127.0.0.1', :user => 'root', :password => '', :db => 'new_database'}
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
- all_database_names = ['example_db_1', 'example_db_2'] # just used so that the --help command is more useful and can print out the database connection names available
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
- # setup defaults -- it will use these databases by default unless you specify otherwise on the command line:
33
- db_from_name = 'example_db_1'
34
- db_to_name = 'example_db_2'
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 -- \nnote that it doesnt yet do indices and you need to still pass --commit for it to do the structure change. 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") do
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
- else
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}").use_result
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}").use_result
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}").use_result rescue nil
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}").use_result
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};").use_result
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}").use_result
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 [these 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 the following columns: #{apparently_lacking.inspect}\n\n
242
- you might get away with dropping the old table and letting it be recreated -- that might also add the right indices\n\n
243
- #{all_indices.inspect}\n != \n#{all_existing_indices.inspect}"
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 (yes/no)?"
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
- ssh_port = db_from_info[:ssh_port] || db_to_info[:ssh_port]
290
- ssh_local_to_port = db_from_info[:ssh_local_to_port] || db_to_info[:ssh_local_to_port] || 3306
291
- ssh_user = db_from_info[:ssh_user] || db_to_info[:ssh_user]
292
- ssh_local_to_host = db_from_info[:ssh_local_to_host] || db_to_info[:ssh_local_to_host] || 'localhost'
293
- ssh_host = db_from_info[:ssh_host] || db_to_info[:ssh_host]
294
- command = "ssh -N #{ssh_port ? '-p ' + ssh_port.to_s : nil} -L 4000:#{ssh_local_to_host}:#{ssh_local_to_port} #{ssh_user}@#{ssh_host} \n" # NOTE DOES NOT YET ALLOW FOR TWO SSH DB's
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").use_result
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").use_result
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.use_result
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.use_result
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.use_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
- require 'pp'
430
- pp "got sql result", result
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.2.0</a>
36
+ <a href="http://rubyforge.org/projects/table_syncer" class="numbers">0.3.0</a>
37
37
  </div>
38
38
  <h1>&amp;#x2192; &#8216;table_syncer&#8217;</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.2.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-09-30 00:00:00 -06:00
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.7.0
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.txt
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.0
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