mysql-inspector 0.0.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/.gitignore +3 -0
- data/README +48 -0
- data/Rakefile +15 -0
- data/VERSION +1 -0
- data/bin/mysql-inspector +76 -0
- data/lib/mysql-inspector.rb +210 -0
- data/mysql-inspector.gemspec +44 -0
- metadata +68 -0
data/.gitignore
ADDED
data/README
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
mysql-inspector is a simple tool for diffing and searching mysql dumps.
|
2
|
+
|
3
|
+
Why do I need that?
|
4
|
+
|
5
|
+
It helps you migrate your schema from one version to another.
|
6
|
+
|
7
|
+
Ok, how?
|
8
|
+
|
9
|
+
Say you have a project called zippers. Your development database is called
|
10
|
+
zippers_development. You're working on a branch called smoother, based on
|
11
|
+
master, which adds a new column metal_grade.
|
12
|
+
|
13
|
+
Start by storing the new state of your database.
|
14
|
+
|
15
|
+
% mysql-inspector --target zippers_development --write
|
16
|
+
|
17
|
+
This command says "store the state of the zippers_development database as my
|
18
|
+
target version".
|
19
|
+
|
20
|
+
Now, go back to your master branch and load its database into
|
21
|
+
zippers_development, then dump the contents.
|
22
|
+
|
23
|
+
% mysql-inspector --current zippers_development --write
|
24
|
+
|
25
|
+
Now, in order to write database migrations for master to smoother let's see
|
26
|
+
what changes occurred.
|
27
|
+
|
28
|
+
% mysql-inspector --diff
|
29
|
+
|
30
|
+
[[ show sample output ]]
|
31
|
+
|
32
|
+
Here we see that our target version contains one column that the current
|
33
|
+
version does not. It's easy to write an alter statement, in fact most of the
|
34
|
+
information is right here.
|
35
|
+
|
36
|
+
mysql% alter table zippers add column metal_grade int(11) NOT NULL DEFAULT '0';
|
37
|
+
|
38
|
+
Now we can compare the two again. This time we need to add the --force argument
|
39
|
+
to say that it's ok to overwrite the previous dump.
|
40
|
+
|
41
|
+
% mysql-inspector --current zippers_development --write --force
|
42
|
+
% mysql-inspector --diff
|
43
|
+
|
44
|
+
No differences!
|
45
|
+
|
46
|
+
That was pretty simple. Is there more?
|
47
|
+
|
48
|
+
Sure.
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'rake'
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'jeweler'
|
5
|
+
Jeweler::Tasks.new do |gemspec|
|
6
|
+
gemspec.name = "mysql-inspector"
|
7
|
+
gemspec.summary = "Tools for identifying changes to a MySQL schema"
|
8
|
+
gemspec.email = "ryan@fivesevensix.com"
|
9
|
+
gemspec.homepage = "http://github.com/rcarver/mysql-inspector"
|
10
|
+
gemspec.authors = ["Ryan Carver"]
|
11
|
+
end
|
12
|
+
Jeweler::GemcutterTasks.new
|
13
|
+
rescue LoadError
|
14
|
+
puts "Jeweler not available. Install it with: gem install jeweler"
|
15
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.0
|
data/bin/mysql-inspector
ADDED
@@ -0,0 +1,76 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
require "lib/mysql-inspector"
|
5
|
+
|
6
|
+
options = {
|
7
|
+
:databases => [],
|
8
|
+
:versions => [],
|
9
|
+
:base_dir => ".",
|
10
|
+
:diff => false,
|
11
|
+
:grep => false,
|
12
|
+
:dump => false,
|
13
|
+
:clean => false
|
14
|
+
}
|
15
|
+
|
16
|
+
OptionParser.new do |opts|
|
17
|
+
opts.banner = "Usage: ????"
|
18
|
+
|
19
|
+
opts.on("-c", "--current [db]") do |db_name|
|
20
|
+
options[:versions] << "current"
|
21
|
+
options[:databases] << db_name
|
22
|
+
end
|
23
|
+
|
24
|
+
opts.on("-t", "--target [db]") do |db_name|
|
25
|
+
options[:versions] << "target"
|
26
|
+
options[:databases] << db_name
|
27
|
+
end
|
28
|
+
|
29
|
+
opts.on("-w", "--write") do |yes|
|
30
|
+
options[:dump] = true
|
31
|
+
end
|
32
|
+
|
33
|
+
opts.on("-f", "--fresh") do |ok|
|
34
|
+
options[:clean] = true
|
35
|
+
end
|
36
|
+
|
37
|
+
opts.on("-g", "--grep foo_id,bar_id") do |columns|
|
38
|
+
options[:grep] = columns.split(",").collect { |c| c.strip }
|
39
|
+
end
|
40
|
+
|
41
|
+
opts.on("-d", "--diff") do |info|
|
42
|
+
options[:diff] = true
|
43
|
+
end
|
44
|
+
|
45
|
+
end.parse!
|
46
|
+
|
47
|
+
options[:versions].compact!
|
48
|
+
options[:databases].compact!
|
49
|
+
|
50
|
+
options[:versions].collect! do |version|
|
51
|
+
MysqlInspector::Dump.new(version, options[:base_dir])
|
52
|
+
end
|
53
|
+
|
54
|
+
if options[:clean]
|
55
|
+
options[:versions].each { |v| v.clean! }
|
56
|
+
end
|
57
|
+
|
58
|
+
if options[:dump]
|
59
|
+
raise "Missing database names" unless options[:versions].size == options[:databases].size
|
60
|
+
options[:versions].zip(options[:databases]).each do |v, d|
|
61
|
+
v.dump!(d)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
if options[:grep]
|
66
|
+
options[:versions].each do |v|
|
67
|
+
grep = MysqlInspector::Grep.new(v)
|
68
|
+
grep.find(STDOUT, *options[:grep])
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
if options[:diff]
|
73
|
+
inputs = ["current", "target"].collect { |version| MysqlInspector::Dump.new(version, options[:base_dir]) }
|
74
|
+
comparison = MysqlInspector::Comparison.new(*inputs)
|
75
|
+
comparison.compare(STDOUT)
|
76
|
+
end
|
@@ -0,0 +1,210 @@
|
|
1
|
+
require "fileutils"
|
2
|
+
|
3
|
+
module MysqlInspector
|
4
|
+
|
5
|
+
module Config
|
6
|
+
extend self
|
7
|
+
|
8
|
+
def mysqldump(*args)
|
9
|
+
all_args = ["-u #{mysql_user}"] + args
|
10
|
+
Command.new(mysqldump_path, *all_args)
|
11
|
+
end
|
12
|
+
|
13
|
+
def mysql_user
|
14
|
+
@mysql_user ||= "root"
|
15
|
+
end
|
16
|
+
|
17
|
+
def mysqldump_path
|
18
|
+
@mysqldump_path ||= begin
|
19
|
+
path = `which mysqldump`.chomp
|
20
|
+
raise "mysqldump was not in your path" if path.empty?
|
21
|
+
path
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class Command
|
27
|
+
def initialize(path, *args)
|
28
|
+
@path = path
|
29
|
+
@args = args
|
30
|
+
end
|
31
|
+
def to_s
|
32
|
+
"#{@path} #{@args * " "}"
|
33
|
+
end
|
34
|
+
def run!
|
35
|
+
system to_s
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
class Dump
|
40
|
+
def initialize(version, base_dir)
|
41
|
+
@version = version
|
42
|
+
@base_dir = base_dir
|
43
|
+
# TODO: sanity check base_dir for either a relative dir or /tmp/...
|
44
|
+
end
|
45
|
+
|
46
|
+
attr_reader :version, :base_dir
|
47
|
+
|
48
|
+
def db_name
|
49
|
+
@db_name ||= read_db_name
|
50
|
+
end
|
51
|
+
|
52
|
+
def dir
|
53
|
+
File.join(base_dir, version)
|
54
|
+
end
|
55
|
+
|
56
|
+
def clean!
|
57
|
+
FileUtils.rm_rf(dir)
|
58
|
+
end
|
59
|
+
|
60
|
+
def dump!(db_name)
|
61
|
+
raise "Destination exists! (#{dir.inspect})" if File.exist?(dir)
|
62
|
+
@db_name = db_name
|
63
|
+
FileUtils.mkdir_p(dir)
|
64
|
+
Config.mysqldump("--no-data", "-T #{dir}", "--skip-opt", db_name).run!
|
65
|
+
File.open(info_file, "w") { |f| f.puts(db_name) }
|
66
|
+
end
|
67
|
+
|
68
|
+
protected
|
69
|
+
|
70
|
+
def info_file
|
71
|
+
File.join(dir, ".info")
|
72
|
+
end
|
73
|
+
|
74
|
+
def read_db_name
|
75
|
+
raise "No dump exists at #{dir.inspect}" unless File.exist?(info_file)
|
76
|
+
File.read(info_file).strip
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
module Utils
|
81
|
+
|
82
|
+
def file_to_table(file)
|
83
|
+
file[/(.*)\.sql/, 1]
|
84
|
+
end
|
85
|
+
|
86
|
+
def sanitize_schema!(schema)
|
87
|
+
schema.collect! { |line| line.rstrip[/(.*?),?$/, 1] }
|
88
|
+
schema.delete_if { |line| line =~ /(\/\*|--|CREATE TABLE)/ or line == ");" or line.strip.empty? }
|
89
|
+
schema.sort!
|
90
|
+
schema
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class Grep
|
95
|
+
include Utils
|
96
|
+
|
97
|
+
def initialize(dump)
|
98
|
+
@dump = dump
|
99
|
+
end
|
100
|
+
|
101
|
+
attr_reader :dump
|
102
|
+
|
103
|
+
def find(writer, *matchers)
|
104
|
+
writer.puts
|
105
|
+
writer.puts "Searching #{dump.version} (#{dump.db_name}) for #{matchers.inspect}"
|
106
|
+
writer.puts
|
107
|
+
files = Dir[File.join(dump.dir, "*.sql")].collect { |f| File.basename(f) }.sort
|
108
|
+
files.each do |f|
|
109
|
+
schema = File.read(File.join(dump.dir, f)).split("\n")
|
110
|
+
sanitize_schema!(schema)
|
111
|
+
|
112
|
+
matches = schema.select do |line|
|
113
|
+
matchers.all? do |matcher|
|
114
|
+
col, *items = matcher.split(/\s+/)
|
115
|
+
col = "`#{col}`"
|
116
|
+
[col, items].flatten.all? { |item| line.downcase =~ /#{Regexp.escape item.downcase}/ }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
if matches.any?
|
121
|
+
writer.puts
|
122
|
+
writer.puts file_to_table(f)
|
123
|
+
writer.puts "*" * file_to_table(f).size
|
124
|
+
writer.puts
|
125
|
+
writer.puts "Found matching:"
|
126
|
+
writer.puts matches.join("\n")
|
127
|
+
writer.puts
|
128
|
+
writer.puts "Full schema:"
|
129
|
+
writer.puts schema.join("\n")
|
130
|
+
writer.puts
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
class Comparison
|
137
|
+
include Utils
|
138
|
+
|
139
|
+
def initialize(current, target)
|
140
|
+
@current = current
|
141
|
+
@target = target
|
142
|
+
end
|
143
|
+
|
144
|
+
attr_reader :current, :target
|
145
|
+
|
146
|
+
def ignore_files
|
147
|
+
["migration_info.sql"]
|
148
|
+
end
|
149
|
+
|
150
|
+
def compare(writer=STDOUT)
|
151
|
+
writer.puts
|
152
|
+
writer.puts "Current: #{current.version} (#{current.db_name})"
|
153
|
+
writer.puts "Target: #{target.version} (#{target.db_name})"
|
154
|
+
|
155
|
+
current_files = Dir[File.join(current.dir, "*.sql")].collect { |f| File.basename(f) }.sort
|
156
|
+
target_files = Dir[File.join(target.dir, "*.sql")].collect { |f| File.basename(f) }.sort
|
157
|
+
|
158
|
+
# Ignore some tables
|
159
|
+
current_files -= ignore_files
|
160
|
+
target_files -= ignore_files
|
161
|
+
|
162
|
+
files_only_in_target = target_files - current_files
|
163
|
+
files_only_in_current = current_files - target_files
|
164
|
+
common_files = target_files & current_files
|
165
|
+
|
166
|
+
if files_only_in_current.any?
|
167
|
+
writer.puts
|
168
|
+
writer.puts "Tables only in current"
|
169
|
+
writer.puts files_only_in_current.collect { |f| file_to_table(f) }.join(", ")
|
170
|
+
end
|
171
|
+
|
172
|
+
if files_only_in_target.any?
|
173
|
+
writer.puts
|
174
|
+
writer.puts "Tables in target but not in current"
|
175
|
+
writer.puts files_only_in_target.collect { |f| file_to_table(f) }.join(", ")
|
176
|
+
end
|
177
|
+
|
178
|
+
common_files.each do |f|
|
179
|
+
current_schema = File.read(File.join(current.dir, f)).split("\n")
|
180
|
+
target_schema = File.read(File.join(target.dir, f)).split("\n")
|
181
|
+
|
182
|
+
sanitize_schema!(current_schema)
|
183
|
+
sanitize_schema!(target_schema)
|
184
|
+
|
185
|
+
next if current_schema == target_schema
|
186
|
+
|
187
|
+
writer.puts
|
188
|
+
writer.puts file_to_table(f)
|
189
|
+
writer.puts "*" * file_to_table(f).size
|
190
|
+
writer.puts
|
191
|
+
|
192
|
+
only_in_target = target_schema - current_schema
|
193
|
+
only_in_current = current_schema - target_schema
|
194
|
+
|
195
|
+
if only_in_current.any?
|
196
|
+
writer.puts "only in current"
|
197
|
+
writer.puts only_in_current.join("\n")
|
198
|
+
writer.puts
|
199
|
+
end
|
200
|
+
if only_in_target.any?
|
201
|
+
writer.puts "only in target"
|
202
|
+
writer.puts only_in_target.join("\n")
|
203
|
+
writer.puts
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
|
208
|
+
end
|
209
|
+
|
210
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# Generated by jeweler
|
2
|
+
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
+
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
|
+
# -*- encoding: utf-8 -*-
|
5
|
+
|
6
|
+
Gem::Specification.new do |s|
|
7
|
+
s.name = %q{mysql-inspector}
|
8
|
+
s.version = "0.0.0"
|
9
|
+
|
10
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
|
+
s.authors = ["Ryan Carver"]
|
12
|
+
s.date = %q{2010-03-03}
|
13
|
+
s.default_executable = %q{mysql-inspector}
|
14
|
+
s.email = %q{ryan@fivesevensix.com}
|
15
|
+
s.executables = ["mysql-inspector"]
|
16
|
+
s.extra_rdoc_files = [
|
17
|
+
"README"
|
18
|
+
]
|
19
|
+
s.files = [
|
20
|
+
".gitignore",
|
21
|
+
"README",
|
22
|
+
"Rakefile",
|
23
|
+
"VERSION",
|
24
|
+
"bin/mysql-inspector",
|
25
|
+
"lib/mysql-inspector.rb",
|
26
|
+
"mysql-inspector.gemspec"
|
27
|
+
]
|
28
|
+
s.homepage = %q{http://github.com/rcarver/mysql-inspector}
|
29
|
+
s.rdoc_options = ["--charset=UTF-8"]
|
30
|
+
s.require_paths = ["lib"]
|
31
|
+
s.rubygems_version = %q{1.3.6}
|
32
|
+
s.summary = %q{Tools for identifying changes to a MySQL schema}
|
33
|
+
|
34
|
+
if s.respond_to? :specification_version then
|
35
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
36
|
+
s.specification_version = 3
|
37
|
+
|
38
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
39
|
+
else
|
40
|
+
end
|
41
|
+
else
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
metadata
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: mysql-inspector
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
version: 0.0.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Ryan Carver
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-03-03 00:00:00 -08:00
|
18
|
+
default_executable: mysql-inspector
|
19
|
+
dependencies: []
|
20
|
+
|
21
|
+
description:
|
22
|
+
email: ryan@fivesevensix.com
|
23
|
+
executables:
|
24
|
+
- mysql-inspector
|
25
|
+
extensions: []
|
26
|
+
|
27
|
+
extra_rdoc_files:
|
28
|
+
- README
|
29
|
+
files:
|
30
|
+
- .gitignore
|
31
|
+
- README
|
32
|
+
- Rakefile
|
33
|
+
- VERSION
|
34
|
+
- bin/mysql-inspector
|
35
|
+
- lib/mysql-inspector.rb
|
36
|
+
- mysql-inspector.gemspec
|
37
|
+
has_rdoc: true
|
38
|
+
homepage: http://github.com/rcarver/mysql-inspector
|
39
|
+
licenses: []
|
40
|
+
|
41
|
+
post_install_message:
|
42
|
+
rdoc_options:
|
43
|
+
- --charset=UTF-8
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ">="
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
segments:
|
51
|
+
- 0
|
52
|
+
version: "0"
|
53
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - ">="
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
version: "0"
|
60
|
+
requirements: []
|
61
|
+
|
62
|
+
rubyforge_project:
|
63
|
+
rubygems_version: 1.3.6
|
64
|
+
signing_key:
|
65
|
+
specification_version: 3
|
66
|
+
summary: Tools for identifying changes to a MySQL schema
|
67
|
+
test_files: []
|
68
|
+
|