koke-mydiff 0.0.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.
- 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
|