koke-mydiff 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +4 -0
- data/Manifest.txt +10 -0
- data/README.txt +51 -0
- data/Rakefile +30 -0
- data/bin/mydiff +61 -0
- data/lib/mydiff.rb +230 -0
- data/lib/mydiff/change.rb +105 -0
- data/lib/mydiff/cli.rb +204 -0
- data/test/helper.rb +7 -0
- data/test/test_mydiff.rb +11 -0
- metadata +91 -0
data/History.txt
ADDED
data/Manifest.txt
ADDED
data/README.txt
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
= MyDiff MySQL diff library
|
2
|
+
|
3
|
+
http://github.com/koke/mydiff
|
4
|
+
|
5
|
+
== DESCRIPTION:
|
6
|
+
|
7
|
+
MySQL diff library
|
8
|
+
|
9
|
+
*WARNING* Although this code is public, it's not complete yet.
|
10
|
+
|
11
|
+
== FEATURES/PROBLEMS:
|
12
|
+
|
13
|
+
* FIXME
|
14
|
+
|
15
|
+
== SYNOPSIS:
|
16
|
+
|
17
|
+
mydiff -o mydiff -n mydiff_new
|
18
|
+
|
19
|
+
== REQUIREMENTS:
|
20
|
+
|
21
|
+
* mysql
|
22
|
+
* highline
|
23
|
+
|
24
|
+
== INSTALL:
|
25
|
+
|
26
|
+
* sudo gem install mydiff
|
27
|
+
|
28
|
+
== LICENSE:
|
29
|
+
|
30
|
+
(The MIT License)
|
31
|
+
|
32
|
+
Copyright (c) 2008 FIX
|
33
|
+
|
34
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
35
|
+
a copy of this software and associated documentation files (the
|
36
|
+
'Software'), to deal in the Software without restriction, including
|
37
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
38
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
39
|
+
permit persons to whom the Software is furnished to do so, subject to
|
40
|
+
the following conditions:
|
41
|
+
|
42
|
+
The above copyright notice and this permission notice shall be
|
43
|
+
included in all copies or substantial portions of the Software.
|
44
|
+
|
45
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
46
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
47
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
48
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
49
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
50
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
51
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require "rake/gempackagetask"
|
3
|
+
require "rake/rdoctask"
|
4
|
+
require "rake/testtask"
|
5
|
+
require 'hoe'
|
6
|
+
require './lib/mydiff.rb'
|
7
|
+
|
8
|
+
Hoe.new('mydiff', MyDiff::VERSION) do |p|
|
9
|
+
p.author = 'Jorge Bernal'
|
10
|
+
p.email = 'jbernal@warp.es'
|
11
|
+
p.summary = 'MySQL diff library'
|
12
|
+
p.description = p.paragraphs_of('README.txt', 2..2).join("\n\n")
|
13
|
+
p.url = p.paragraphs_of('README.txt', 1).first
|
14
|
+
p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
|
15
|
+
p.extra_deps << ['mysql','>= 2.7']
|
16
|
+
p.extra_deps << ['highline', '>= 1.4.0']
|
17
|
+
p.remote_rdoc_dir = ''
|
18
|
+
end
|
19
|
+
|
20
|
+
desc "Open an irb session preloaded with this library"
|
21
|
+
task :console do
|
22
|
+
sh "irb -rubygems -r ./lib/mydiff.rb"
|
23
|
+
end
|
24
|
+
|
25
|
+
desc "Run coverage tests"
|
26
|
+
task :coverage do
|
27
|
+
system("rm -fr coverage")
|
28
|
+
system("rcov test/test_*.rb")
|
29
|
+
system("open coverage/index.html")
|
30
|
+
end
|
data/bin/mydiff
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
if File.directory?("lib")
|
4
|
+
$: << "lib"
|
5
|
+
end
|
6
|
+
|
7
|
+
require "rubygems"
|
8
|
+
require "mydiff"
|
9
|
+
require "optparse"
|
10
|
+
|
11
|
+
|
12
|
+
$DEBUG = false
|
13
|
+
|
14
|
+
config = {
|
15
|
+
:host => "localhost",
|
16
|
+
:user => "root",
|
17
|
+
:password => nil
|
18
|
+
}
|
19
|
+
|
20
|
+
opts = OptionParser.new do |opts|
|
21
|
+
opts.banner = "Usage: #{File.basename(__FILE__)} [options] -o OLDDB -n NEWDB"
|
22
|
+
|
23
|
+
opts.on("-d", "--debug", "Show debug messages") do |v|
|
24
|
+
$DEBUG = v
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on("-o", "--olddb OLDDB", "Database to apply changes") do |o|
|
28
|
+
config[:olddb] = o
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on("-n", "--newdb NEWDB", "Database to read changes") do |n|
|
32
|
+
config[:newdb] = n
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on("-h", "--host name", "MySQL host to connect (default #{config[:host]})") do |o|
|
36
|
+
config[:host] = o
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on("-u", "--user name", "MySQL username (default #{config[:user]})") do |o|
|
40
|
+
config[:user] = o
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on("-p", "--password passwd", "MySQL password") do |o|
|
44
|
+
config[:password] = o
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.parse!
|
50
|
+
|
51
|
+
unless config[:newdb] and config[:olddb]
|
52
|
+
puts opts.help
|
53
|
+
exit 1
|
54
|
+
end
|
55
|
+
|
56
|
+
md = MyDiff.new(config)
|
57
|
+
md.cli.main_menu
|
58
|
+
|
59
|
+
# popt = config[:password].nil? ? "" : "-p#{config[:password]}"
|
60
|
+
# system "mysql -h #{config[:host]} -u #{config[:user]} #{popt} #{md.newdb} < new.sql"
|
61
|
+
# system "mysql -h #{config[:host]} -u #{config[:user]} #{popt} #{md.olddb} < old.sql"
|
data/lib/mydiff.rb
ADDED
@@ -0,0 +1,230 @@
|
|
1
|
+
$:.unshift File.dirname(__FILE__)
|
2
|
+
|
3
|
+
require "mysql"
|
4
|
+
require "mydiff/cli"
|
5
|
+
require "mydiff/change"
|
6
|
+
|
7
|
+
# MyDiff helps you to apply changes from one MySQL database to another
|
8
|
+
#
|
9
|
+
# It has some helper methods to
|
10
|
+
#
|
11
|
+
# Example
|
12
|
+
#
|
13
|
+
# md = MyDiff.new(:host => "localhost", :user => "root", :newdb => "mydiff_new", :olddb => "mydiff_old") # => <MyDiff>
|
14
|
+
# md.newdb #=> "mydiff_new"
|
15
|
+
# md.olddb #=> "mydiff_old"
|
16
|
+
# md.new_tables #=> ["new_table1", "new_table2"]
|
17
|
+
# md.dropped_tables #=> ["old_table"]
|
18
|
+
class MyDiff
|
19
|
+
VERSION = '0.0.1'
|
20
|
+
|
21
|
+
# Name of the database with changes
|
22
|
+
attr_accessor :newdb
|
23
|
+
# Name of the current database. Changes will be applied here
|
24
|
+
attr_accessor :olddb
|
25
|
+
# Command Line Interface. See MyDiff::CLI
|
26
|
+
attr_accessor :cli
|
27
|
+
attr_accessor :my #:nodoc:
|
28
|
+
|
29
|
+
# Creates a new MyDiff instance
|
30
|
+
#
|
31
|
+
# Config options
|
32
|
+
# - <tt>:host</tt> - MySQL host
|
33
|
+
# - <tt>:user</tt> - MySQL user
|
34
|
+
# - <tt>:password</tt> - MySQL password
|
35
|
+
# - <tt>:newdb</tt> - Name of the database with the changes to apply
|
36
|
+
# - <tt>:olddb</tt> - Name of the database to apply the changes
|
37
|
+
#
|
38
|
+
# Returns MyDiff
|
39
|
+
def initialize(config)
|
40
|
+
@my = Mysql::new(config[:host], config[:user], config[:password])
|
41
|
+
@newdb = config[:newdb]
|
42
|
+
@olddb = config[:olddb]
|
43
|
+
@cli = CLI.new(self)
|
44
|
+
@fields = {}
|
45
|
+
end
|
46
|
+
|
47
|
+
# Recreates the new database
|
48
|
+
#
|
49
|
+
# *WARNING*: This method drops and recreates the +newdb+ database, you may lose data!
|
50
|
+
def prepare!
|
51
|
+
begin
|
52
|
+
@my.query("DROP DATABASE #{@newdb}")
|
53
|
+
rescue
|
54
|
+
end
|
55
|
+
|
56
|
+
@my.query("CREATE DATABASE #{@newdb}")
|
57
|
+
end
|
58
|
+
|
59
|
+
# Returns an array with table names for the database given in +db+
|
60
|
+
def list_tables(db)
|
61
|
+
@my.select_db(db)
|
62
|
+
@my.list_tables
|
63
|
+
end
|
64
|
+
|
65
|
+
# Returns an array with table names present on +newdb+ but not on +olddb+
|
66
|
+
def new_tables
|
67
|
+
ntables = list_tables(@newdb)
|
68
|
+
otables = list_tables(@olddb)
|
69
|
+
|
70
|
+
ntables.select {|t| not otables.include?(t) }
|
71
|
+
end
|
72
|
+
|
73
|
+
# Returns an array with table names present on +olddb+ but not on +newdb+
|
74
|
+
def dropped_tables
|
75
|
+
ntables = list_tables(@newdb)
|
76
|
+
otables = list_tables(@olddb)
|
77
|
+
|
78
|
+
otables.select {|t| not ntables.include?(t) }
|
79
|
+
end
|
80
|
+
|
81
|
+
# Returns an array with table names present on +newdb+ and +olddb+ which
|
82
|
+
# are different in content
|
83
|
+
def changed_tables
|
84
|
+
ntables = list_tables(@newdb)
|
85
|
+
otables = list_tables(@olddb)
|
86
|
+
|
87
|
+
ntables.select {|t| otables.include?(t) and table_changed?(t) }
|
88
|
+
end
|
89
|
+
|
90
|
+
# Returns an array with rows present in +newdb+ but not in +olddb+, using the +table+ given
|
91
|
+
def new_rows(table)
|
92
|
+
fields = fields_from(table)
|
93
|
+
pkey, fields = extract_pkey_from(fields)
|
94
|
+
my.select_db(@newdb)
|
95
|
+
|
96
|
+
query = "SELECT "
|
97
|
+
query << pkey.collect do |f|
|
98
|
+
"n.#{f["Field"]} #{f["Field"]}"
|
99
|
+
end.join(",")
|
100
|
+
query << ","
|
101
|
+
query << fields.collect do |f|
|
102
|
+
"n.#{f["Field"]} n_#{f["Field"]}"
|
103
|
+
end.join(",")
|
104
|
+
|
105
|
+
query << " FROM #{@newdb}.#{table} AS n LEFT JOIN #{@olddb}.#{table} AS o ON "
|
106
|
+
query << pkey.collect do |f|
|
107
|
+
"n.#{f["Field"]} = o.#{f["Field"]}"
|
108
|
+
end.join(" AND ")
|
109
|
+
query << " WHERE "
|
110
|
+
query << pkey.collect do |f|
|
111
|
+
"o.#{f["Field"]} IS NULL"
|
112
|
+
end.join(" AND ")
|
113
|
+
|
114
|
+
result = my.query(query)
|
115
|
+
new_rows = []
|
116
|
+
while row = result.fetch_hash
|
117
|
+
new_rows << row
|
118
|
+
end
|
119
|
+
new_rows
|
120
|
+
end
|
121
|
+
|
122
|
+
def deleted_rows(table)
|
123
|
+
fields = fields_from(table)
|
124
|
+
pkey, fields = extract_pkey_from(fields)
|
125
|
+
my.select_db(@olddb)
|
126
|
+
|
127
|
+
query = "SELECT "
|
128
|
+
query << pkey.collect do |f|
|
129
|
+
"o.#{f["Field"]} #{f["Field"]}"
|
130
|
+
end.join(",")
|
131
|
+
query << ","
|
132
|
+
query << fields.collect do |f|
|
133
|
+
"o.#{f["Field"]} o_#{f["Field"]}"
|
134
|
+
end.join(",")
|
135
|
+
|
136
|
+
query << " FROM #{@olddb}.#{table} AS o LEFT JOIN #{@newdb}.#{table} AS n ON "
|
137
|
+
query << pkey.collect do |f|
|
138
|
+
"n.#{f["Field"]} = o.#{f["Field"]}"
|
139
|
+
end.join(" AND ")
|
140
|
+
query << " WHERE "
|
141
|
+
query << pkey.collect do |f|
|
142
|
+
"n.#{f["Field"]} IS NULL"
|
143
|
+
end.join(" AND ")
|
144
|
+
|
145
|
+
result = my.query(query)
|
146
|
+
deleted_rows = []
|
147
|
+
while row = result.fetch_hash
|
148
|
+
deleted_rows << row
|
149
|
+
end
|
150
|
+
deleted_rows
|
151
|
+
end
|
152
|
+
|
153
|
+
def changed_rows(table)
|
154
|
+
fields = fields_from(table)
|
155
|
+
pkey, fields = extract_pkey_from(fields)
|
156
|
+
my.select_db(@olddb)
|
157
|
+
|
158
|
+
query = "SELECT "
|
159
|
+
query << pkey.collect do |f|
|
160
|
+
"o.#{f["Field"]} #{f["Field"]}"
|
161
|
+
end.join(",")
|
162
|
+
query << ","
|
163
|
+
query << fields.collect do |f|
|
164
|
+
"o.#{f["Field"]} o_#{f["Field"]}, n.#{f["Field"]} n_#{f["Field"]}"
|
165
|
+
end.join(",")
|
166
|
+
|
167
|
+
query << " FROM #{@olddb}.#{table} AS o INNER JOIN #{@newdb}.#{table} AS n ON "
|
168
|
+
query << pkey.collect do |f|
|
169
|
+
"n.#{f["Field"]} = o.#{f["Field"]}"
|
170
|
+
end.join(" AND ")
|
171
|
+
|
172
|
+
result = my.query(query)
|
173
|
+
changed_rows = []
|
174
|
+
while row = result.fetch_hash
|
175
|
+
changed_rows << row
|
176
|
+
end
|
177
|
+
changed_rows.select do |row|
|
178
|
+
fields.inject(true) do |s,f|
|
179
|
+
s and row["o_#{f["Field"]}"] == row["n_#{f["Field"]}"]
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def fields_from(table)
|
185
|
+
return @fields[table] if @fields[table]
|
186
|
+
@my.select_db(@newdb)
|
187
|
+
res = @my.query("DESCRIBE #{table}")
|
188
|
+
fields = []
|
189
|
+
while (field = res.fetch_hash)
|
190
|
+
fields << field
|
191
|
+
end
|
192
|
+
|
193
|
+
@fields[table] ||= fields
|
194
|
+
end
|
195
|
+
|
196
|
+
def count_rows(db, table)
|
197
|
+
@my.select_db(db)
|
198
|
+
res = @my.query("SELECT COUNT(*) FROM #{table}")
|
199
|
+
res.fetch_row[0]
|
200
|
+
end
|
201
|
+
|
202
|
+
def extract_pkey_from(fields)
|
203
|
+
fields.partition {|f| f["Key"] == "PRI" }
|
204
|
+
end
|
205
|
+
|
206
|
+
def pkey_of(table)
|
207
|
+
fields_from(table).select {|f| f["Key"] == "PRI"}.map {|f| f["Field"]}
|
208
|
+
end
|
209
|
+
|
210
|
+
def data_fields_of(table)
|
211
|
+
fields_from(table).select {|f| f["Key"] != "PRI"}.map {|f| f["Field"]}
|
212
|
+
end
|
213
|
+
|
214
|
+
def checksum_table(db, table)
|
215
|
+
@my.select_db(db)
|
216
|
+
@my.query("CHECKSUM TABLE #{table}").fetch_row[1]
|
217
|
+
end
|
218
|
+
|
219
|
+
def table_changed?(table)
|
220
|
+
checksum_table(@olddb, table) != checksum_table(@newdb, table)
|
221
|
+
end
|
222
|
+
|
223
|
+
def select_new
|
224
|
+
@my.select_db(@newdb)
|
225
|
+
end
|
226
|
+
|
227
|
+
def select_old
|
228
|
+
@my.select_db(@olddb)
|
229
|
+
end
|
230
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
require "yaml"
|
2
|
+
|
3
|
+
class MyDiff
|
4
|
+
# Represents one change to apply to the database
|
5
|
+
class Change
|
6
|
+
# Can be one of <tt>:new</tt>, <tt>:drop</tt>, <tt>:overwrite</tt> or <tt>:patch</tt>
|
7
|
+
attr_accessor :type
|
8
|
+
attr_accessor :chunks
|
9
|
+
|
10
|
+
# Create a new change for a specific table
|
11
|
+
#
|
12
|
+
# +parent+:: the MyDiff parent class
|
13
|
+
# +type+:: one of <tt>:new</tt>, <tt>:drop</tt>, <tt>:overwrite</tt> or <tt>:patch</tt>
|
14
|
+
# +table+:: the table to change
|
15
|
+
def initialize(parent, type, table)
|
16
|
+
@md = parent
|
17
|
+
@type = type
|
18
|
+
@table = table
|
19
|
+
@chunks = {}
|
20
|
+
end
|
21
|
+
|
22
|
+
# Adds a chunk to apply in the change.
|
23
|
+
#
|
24
|
+
# +type+:: can be either <tt>:new</tt>, <tt>:delete</tt> or <tt>:change</tt>
|
25
|
+
# +pkey+:: should be a hash containing primary key fields
|
26
|
+
# +fields+:: should be a hash containint the fields not belonging to primary key (only for +type+ = <tt>:new</tt> or <tt>:change</tt>)
|
27
|
+
def add_chunk(type, pkey, fields = {})
|
28
|
+
raise ArgumentError, "Chunks can be added only if type is :patch" unless @type.eql?(:patch)
|
29
|
+
@chunks[pkey.to_yaml] = { :type => type, :pkey => pkey, :fields => fields}
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete_chunk(pkey)
|
33
|
+
@chunks.delete(pkey.to_yaml)
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_chunk(pkey)
|
37
|
+
@chunks[pkey.to_yaml]
|
38
|
+
end
|
39
|
+
|
40
|
+
def has_chunk?(pkey)
|
41
|
+
@chunks.has_key?(pkey.to_yaml)
|
42
|
+
end
|
43
|
+
|
44
|
+
def apply!
|
45
|
+
if @type.eql?(:new)
|
46
|
+
puts "** Creating #{@table}"
|
47
|
+
@md.select_old
|
48
|
+
@md.my.query("CREATE TABLE #{@table} LIKE #{@md.newdb}.#{@table}")
|
49
|
+
@md.my.query("INSERT INTO #{@table} SELECT * FROM #{@md.newdb}.#{@table}")
|
50
|
+
elsif @type.eql?(:drop)
|
51
|
+
puts "** Dropping table #{@table}"
|
52
|
+
@md.select_old
|
53
|
+
@md.my.query("DROP TABLE #{@table}")
|
54
|
+
elsif @type.eql?(:overwrite)
|
55
|
+
raise NotImplementedError, "Type :overwrite is not yet in use"
|
56
|
+
elsif @type.eql?(:patch)
|
57
|
+
printf "** Updating table #{@table}"
|
58
|
+
@chunks.each_pair do |pkey, data|
|
59
|
+
printf "."
|
60
|
+
@md.select_old
|
61
|
+
if data[:type].eql?(:new)
|
62
|
+
field_list = []
|
63
|
+
data_list = []
|
64
|
+
data[:pkey].each_pair do |k,v|
|
65
|
+
field_list << k
|
66
|
+
data_list << "'" + @md.my.escape_string(v) + "'"
|
67
|
+
end
|
68
|
+
data[:fields].each_pair do |k,v|
|
69
|
+
field_list << k
|
70
|
+
data_list << "'" + @md.my.escape_string(v) + "'"
|
71
|
+
end
|
72
|
+
|
73
|
+
puts("INSERT INTO #{@table} (#{field_list.join(',')}) VALUES(#{data_list.join(',')})") if $DEBUG
|
74
|
+
@md.my.query("INSERT INTO #{@table} (#{field_list.join(',')}) VALUES(#{data_list.join(',')})")
|
75
|
+
elsif data[:type].eql?(:change)
|
76
|
+
changes_list = []
|
77
|
+
pkey_list = []
|
78
|
+
data[:pkey].each_pair do |k,v|
|
79
|
+
pkey_list << "#{k} = '" + @md.my.escape_string(v) + "'"
|
80
|
+
end
|
81
|
+
data[:fields].each_pair do |k,v|
|
82
|
+
changes_list << "#{k} = '" + @md.my.escape_string(v) + "'"
|
83
|
+
end
|
84
|
+
|
85
|
+
puts("UPDATE #{@table} SET #{changes_list.join(',')} WHERE #{pkey_list.join(' AND ')}") if $DEBUG
|
86
|
+
@md.my.query("UPDATE #{@table} SET #{changes_list.join(',')} WHERE #{pkey_list.join(' AND ')}")
|
87
|
+
elsif data[:type].eql?(:delete)
|
88
|
+
pkey_list = []
|
89
|
+
data[:pkey].each_pair do |k,v|
|
90
|
+
pkey_list << "#{k} = '" + @md.my.escape_string(v) + "'"
|
91
|
+
end
|
92
|
+
puts("DELETE FROM #{@table} WHERE #{pkey_list.join(' AND ')}") if $DEBUG
|
93
|
+
@md.my.query("DELETE FROM #{@table} WHERE #{pkey_list.join(' AND ')}")
|
94
|
+
else
|
95
|
+
raise NotImplementedError, "Invalid chunk type: #{data[:type]}"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
puts
|
100
|
+
else
|
101
|
+
raise NotImplementedError, "Invalid type #{@type.to_s}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/mydiff/cli.rb
ADDED
@@ -0,0 +1,204 @@
|
|
1
|
+
require "highline"
|
2
|
+
|
3
|
+
class MyDiff
|
4
|
+
class CLI < HighLine
|
5
|
+
def initialize(parent)
|
6
|
+
@md = parent
|
7
|
+
@tables = []
|
8
|
+
@md.new_tables.each {|t| @tables << {:status => :new, :table => t}}
|
9
|
+
@md.dropped_tables.each {|t| @tables << {:status => :drop, :table => t}}
|
10
|
+
@md.changed_tables.each {|t| @tables << {:status => :change, :table => t}}
|
11
|
+
|
12
|
+
@tables.sort! {|t1,t2| t1[:table] <=> t2[:table]}
|
13
|
+
@changes = {}
|
14
|
+
super()
|
15
|
+
end
|
16
|
+
|
17
|
+
def main_menu
|
18
|
+
continue = true
|
19
|
+
begin
|
20
|
+
while continue
|
21
|
+
choose do |menu|
|
22
|
+
menu.prompt = "What now?"
|
23
|
+
|
24
|
+
menu.choice("Status") { status }
|
25
|
+
menu.choice("Accept") { accept }
|
26
|
+
menu.choice("Reject") { reject }
|
27
|
+
menu.choice("Patch") { patch }
|
28
|
+
menu.choice("Apply") { apply! }
|
29
|
+
menu.choice("Quit") { continue = false }
|
30
|
+
end
|
31
|
+
end
|
32
|
+
rescue Interrupt
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def status(type = :all)
|
37
|
+
type = [type] unless type.is_a?(Array)
|
38
|
+
@tables.each_with_index do |table, count|
|
39
|
+
if type.include?(:all) or type.include?(table[:status])
|
40
|
+
render_row(count, table[:table], table[:status])
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def accept
|
46
|
+
submenu do |table|
|
47
|
+
change = @changes[table[:table]]
|
48
|
+
if change #and change.type.eql?(:patch)
|
49
|
+
@changes.delete(table[:table])
|
50
|
+
end
|
51
|
+
@changes[table[:table]] = Change.new(@md, :add, table[:table])
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def reject
|
56
|
+
submenu do |table|
|
57
|
+
change = @changes[table[:table]]
|
58
|
+
if change #and change.type.eql?(:patch)
|
59
|
+
@changes.delete(table[:table])
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def patch
|
65
|
+
submenu([:change, :new]) do |table|
|
66
|
+
continue = true
|
67
|
+
while continue
|
68
|
+
rows = []
|
69
|
+
@md.deleted_rows(table[:table]).each {|r| rows << {:status => :delete, :row => r}}
|
70
|
+
@md.changed_rows(table[:table]).each {|r| rows << {:status => :change, :row => r}}
|
71
|
+
@md.new_rows(table[:table]).each {|r| rows << {:status => :new, :row => r}}
|
72
|
+
rows.each_with_index do |row, count|
|
73
|
+
status = row[:status]
|
74
|
+
row = row[:row]
|
75
|
+
pkey = {}
|
76
|
+
@md.pkey_of(table[:table]).each do |f|
|
77
|
+
pkey[f] = row[f]
|
78
|
+
end
|
79
|
+
ndata = @md.data_fields_of(table[:table]).map do |f|
|
80
|
+
row["n_#{f}"]
|
81
|
+
end
|
82
|
+
odata = @md.data_fields_of(table[:table]).map do |f|
|
83
|
+
row["o_#{f}"]
|
84
|
+
end
|
85
|
+
|
86
|
+
begin
|
87
|
+
if @changes[table[:table]].has_chunk?(pkey)
|
88
|
+
status = "*"
|
89
|
+
else
|
90
|
+
status = " "
|
91
|
+
end
|
92
|
+
rescue
|
93
|
+
status = " "
|
94
|
+
end
|
95
|
+
say ""
|
96
|
+
say "%2d. [%s] (%s) %s" % [count, status, pkey.values.join("||"), odata.join("||")]
|
97
|
+
puts " [%s] (%s) %s" % [status, pkey.values.join("||"), ndata.join("||")]
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
opt = ask "Which one?"
|
102
|
+
if opt.empty? or opt.downcase.eql?("q")
|
103
|
+
continue = false
|
104
|
+
else
|
105
|
+
if opt =~ /(\d+)-(\d+)/
|
106
|
+
opts = ($1..$2).to_a
|
107
|
+
elsif opt =~ /(\d+)/
|
108
|
+
opts = [$1]
|
109
|
+
end
|
110
|
+
opts.each do |opt|
|
111
|
+
row = rows[opt.to_i]
|
112
|
+
if row.nil? or not opt =~ /[0-9+]/
|
113
|
+
say "Wrong answer!"
|
114
|
+
next
|
115
|
+
end
|
116
|
+
change = @changes[table[:table]]
|
117
|
+
if (change and not change.type.eql?(:patch)) or not change
|
118
|
+
@changes.delete(table[:table])
|
119
|
+
change = Change.new(@md, :patch, table[:table])
|
120
|
+
end
|
121
|
+
|
122
|
+
pkey = {}
|
123
|
+
@md.pkey_of(table[:table]).each do |f|
|
124
|
+
pkey[f] = row[:row][f]
|
125
|
+
end
|
126
|
+
if change.has_chunk?(pkey)
|
127
|
+
change.delete_chunk(pkey)
|
128
|
+
else
|
129
|
+
data = {}
|
130
|
+
@md.data_fields_of(table[:table]).each do |f|
|
131
|
+
data[f] = row[:row]["n_#{f}"]
|
132
|
+
end
|
133
|
+
change.add_chunk(row[:status], pkey, data)
|
134
|
+
end
|
135
|
+
|
136
|
+
@changes[table[:table]] = change
|
137
|
+
if change.chunks.empty?
|
138
|
+
@changes.delete(table[:table])
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
def apply!
|
147
|
+
@changes.each_value do |change|
|
148
|
+
change.apply!
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
private
|
153
|
+
def submenu(type = :all)
|
154
|
+
continue = true
|
155
|
+
while continue
|
156
|
+
status(type)
|
157
|
+
opt = ask "Which one?"
|
158
|
+
if opt.empty? or opt.downcase.eql?("q")
|
159
|
+
continue = false
|
160
|
+
else
|
161
|
+
if opt =~ /(\d+)-(\d+)/
|
162
|
+
opts = ($1..$2).to_a
|
163
|
+
elsif opt =~ /(\d+)/
|
164
|
+
opts = [$1]
|
165
|
+
end
|
166
|
+
opts.each do |opt|
|
167
|
+
table = @tables[opt.to_i]
|
168
|
+
if table.nil? or not opt =~ /[0-9+]/
|
169
|
+
say "Wrong answer!"
|
170
|
+
next
|
171
|
+
end
|
172
|
+
yield table
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def render_row(count, table, status)
|
179
|
+
if @changes.has_key?(table)
|
180
|
+
if @changes[table].type.eql?(:patch)
|
181
|
+
change = "/"
|
182
|
+
else
|
183
|
+
change = "*"
|
184
|
+
end
|
185
|
+
else
|
186
|
+
change = " "
|
187
|
+
end
|
188
|
+
|
189
|
+
if status.eql?(:new)
|
190
|
+
say "%2d. [%s] N %8d %s" % [count, change, "+" + @md.count_rows(@md.newdb, table), table]
|
191
|
+
elsif status.eql?(:drop)
|
192
|
+
say "%2d. [%s] D %8d %s" % [count, change, "-" + @md.count_rows(@md.olddb, table), table]
|
193
|
+
elsif status.eql?(:change)
|
194
|
+
new_rows = @md.new_rows(table).size
|
195
|
+
deleted_rows = @md.deleted_rows(table).size
|
196
|
+
changed_rows = @md.changed_rows(table).size
|
197
|
+
new_rows += changed_rows
|
198
|
+
deleted_rows += changed_rows
|
199
|
+
changed_rows = "+#{new_rows}/-#{deleted_rows}"
|
200
|
+
say "%2d. [%s] C %8s %s" % [count, change, changed_rows, table]
|
201
|
+
end
|
202
|
+
end
|
203
|
+
end
|
204
|
+
end
|
data/test/helper.rb
ADDED
data/test/test_mydiff.rb
ADDED
metadata
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: koke-mydiff
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Jorge Bernal
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2008-05-13 00:00:00 -07:00
|
13
|
+
default_executable: mydiff
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: mysql
|
17
|
+
version_requirement:
|
18
|
+
version_requirements: !ruby/object:Gem::Requirement
|
19
|
+
requirements:
|
20
|
+
- - ">="
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: "2.7"
|
23
|
+
version:
|
24
|
+
- !ruby/object:Gem::Dependency
|
25
|
+
name: highline
|
26
|
+
version_requirement:
|
27
|
+
version_requirements: !ruby/object:Gem::Requirement
|
28
|
+
requirements:
|
29
|
+
- - ">="
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: 1.4.0
|
32
|
+
version:
|
33
|
+
- !ruby/object:Gem::Dependency
|
34
|
+
name: hoe
|
35
|
+
version_requirement:
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: 1.5.1
|
41
|
+
version:
|
42
|
+
description: "== DESCRIPTION:"
|
43
|
+
email: jbernal@warp.es
|
44
|
+
executables:
|
45
|
+
- mydiff
|
46
|
+
extensions: []
|
47
|
+
|
48
|
+
extra_rdoc_files:
|
49
|
+
- History.txt
|
50
|
+
- Manifest.txt
|
51
|
+
- README.txt
|
52
|
+
files:
|
53
|
+
- History.txt
|
54
|
+
- Manifest.txt
|
55
|
+
- README.txt
|
56
|
+
- Rakefile
|
57
|
+
- bin/mydiff
|
58
|
+
- lib/mydiff/change.rb
|
59
|
+
- lib/mydiff/cli.rb
|
60
|
+
- lib/mydiff.rb
|
61
|
+
- test/helper.rb
|
62
|
+
- test/test_mydiff.rb
|
63
|
+
has_rdoc: true
|
64
|
+
homepage: http://github.com/koke/mydiff
|
65
|
+
post_install_message:
|
66
|
+
rdoc_options:
|
67
|
+
- --main
|
68
|
+
- README.txt
|
69
|
+
require_paths:
|
70
|
+
- lib
|
71
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: "0"
|
76
|
+
version:
|
77
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: "0"
|
82
|
+
version:
|
83
|
+
requirements: []
|
84
|
+
|
85
|
+
rubyforge_project: mydiff
|
86
|
+
rubygems_version: 1.0.1
|
87
|
+
signing_key:
|
88
|
+
specification_version: 2
|
89
|
+
summary: MySQL diff library
|
90
|
+
test_files:
|
91
|
+
- test/test_mydiff.rb
|