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 ADDED
@@ -0,0 +1,3 @@
1
+ current
2
+ target
3
+ pkg
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
@@ -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
+