mysql-inspector 0.0.6 → 0.1.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 +5 -3
- data/CHANGELOG.md +8 -0
- data/Gemfile +6 -0
- data/LICENSE +20 -0
- data/README.md +82 -0
- data/Rakefile +36 -14
- data/bin/mysql-inspector +9 -85
- data/lib/mysql-inspector.rb +1 -226
- data/lib/mysql_inspector.rb +30 -0
- data/lib/mysql_inspector/access.rb +64 -0
- data/lib/mysql_inspector/ar/access.rb +55 -0
- data/lib/mysql_inspector/cli.rb +291 -0
- data/lib/mysql_inspector/column.rb +21 -0
- data/lib/mysql_inspector/config.rb +82 -0
- data/lib/mysql_inspector/constraint.rb +28 -0
- data/lib/mysql_inspector/diff.rb +82 -0
- data/lib/mysql_inspector/dump.rb +70 -0
- data/lib/mysql_inspector/grep.rb +65 -0
- data/lib/mysql_inspector/index.rb +25 -0
- data/lib/mysql_inspector/migrations.rb +37 -0
- data/lib/mysql_inspector/railtie.rb +17 -0
- data/lib/mysql_inspector/railties/databases.rake +92 -0
- data/lib/mysql_inspector/table.rb +147 -0
- data/lib/mysql_inspector/table_part.rb +21 -0
- data/lib/mysql_inspector/version.rb +3 -0
- data/mysql-inspector.gemspec +17 -36
- data/test/fixtures/migrate/111_create_users.rb +7 -0
- data/test/fixtures/migrate/222_create_things.rb +9 -0
- data/test/helper.rb +125 -0
- data/test/helper_ar.rb +37 -0
- data/test/helpers/mysql_schemas.rb +82 -0
- data/test/helpers/mysql_utils.rb +35 -0
- data/test/helpers/string_unindented.rb +13 -0
- data/test/mysql_inspector/cli_basics_test.rb +77 -0
- data/test/mysql_inspector/cli_diff_test.rb +60 -0
- data/test/mysql_inspector/cli_grep_test.rb +74 -0
- data/test/mysql_inspector/cli_load_test.rb +43 -0
- data/test/mysql_inspector/cli_write_test.rb +58 -0
- data/test/mysql_inspector/config_test.rb +14 -0
- data/test/mysql_inspector/diff_test.rb +82 -0
- data/test/mysql_inspector/dump_test.rb +81 -0
- data/test/mysql_inspector/grep_test.rb +61 -0
- data/test/mysql_inspector/table_test.rb +123 -0
- data/test/mysql_inspector_ar/ar_dump_test.rb +29 -0
- data/test/mysql_inspector_ar/ar_migrations_test.rb +47 -0
- metadata +123 -49
- data/README +0 -48
- data/VERSION +0 -1
data/.gitignore
CHANGED
data/CHANGELOG.md
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Ryan Carver
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,82 @@
|
|
1
|
+
# mysql-inspector
|
2
|
+
|
3
|
+
mysql-inspector is a command line tool that helps you understand your
|
4
|
+
MySQL database schema. It works by writing a special type of dump file
|
5
|
+
to disk and then parsing it for various purposes.
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
mysql-inspector supports several useful commands, but the first thing
|
10
|
+
you'll want to do is write at least one copy of your database to disk.
|
11
|
+
|
12
|
+
### write
|
13
|
+
|
14
|
+
Write a copy of your database to disk for mysql-inspector to operate on.
|
15
|
+
You can name this copy whatever you want, by default it's called
|
16
|
+
`current`.
|
17
|
+
|
18
|
+
mysql-inspector write my_database
|
19
|
+
|
20
|
+
The result of this command will be a directory full of `.table` files at
|
21
|
+
`./current`, one for each table in your database. A `.table` file is a
|
22
|
+
simplified and consistent representation of a table. Most importantly,
|
23
|
+
it will not change arbitrarly like a `mysqldump` file if the order of
|
24
|
+
columns changes or an `AUTO_INCREMENT` value is defined on the table.
|
25
|
+
mysql-inspector is purely concerned with the relational structure of the
|
26
|
+
table and favors this over an exact representation of the current
|
27
|
+
database schema. In practice, this means that you can commit this
|
28
|
+
directory to source control and easily view diffs over time without
|
29
|
+
excess line noise.
|
30
|
+
|
31
|
+
### grep
|
32
|
+
|
33
|
+
Search your entire database for columns, indices and constraints that
|
34
|
+
match a string or regex. For example, find everything that includes
|
35
|
+
'user_id' to see which tables relate to a user.
|
36
|
+
|
37
|
+
mysql-inspector grep user_id
|
38
|
+
mysql-inspector grep '^name'
|
39
|
+
|
40
|
+
Multiple matchers may be specified, which are AND'd together.
|
41
|
+
|
42
|
+
mysql-inspector grep first name
|
43
|
+
|
44
|
+
### diff
|
45
|
+
|
46
|
+
Compare two schemas against each other. Perhaps your local development
|
47
|
+
and production databases have gone out of sync. First write a copy of
|
48
|
+
each and then let mysql-inspector show you the tables and attributes
|
49
|
+
that differ.
|
50
|
+
|
51
|
+
By default, a diff is performed on dumps named `current` and `target`.
|
52
|
+
|
53
|
+
mysql-inspector write dev_database current
|
54
|
+
mysql-inspector write prod_database target
|
55
|
+
mysql-inspector diff
|
56
|
+
|
57
|
+
### load
|
58
|
+
|
59
|
+
Restore a version of your database schema. By default, the `current`
|
60
|
+
schema is used.
|
61
|
+
|
62
|
+
mysql-inspector load my_database
|
63
|
+
|
64
|
+
## Rails and ActiveRecord Migrations
|
65
|
+
|
66
|
+
mysql-inspector can help you manage your database schema in a Rails
|
67
|
+
project. It replaces rake tasks such as `db:structure:dump` and writes
|
68
|
+
its own version at `db/current` instead of `db/structure.sql`. You'll
|
69
|
+
find this format much more convenient for checking into version control.
|
70
|
+
|
71
|
+
When a `schema_migrations` table is found, mysql-inspector writes its
|
72
|
+
contents to a file called `schema_migrations` within the dump directory.
|
73
|
+
When a dump is loaded via `mysql-inspector load` or `rake
|
74
|
+
db:structure:load`, the migrations will be restored.
|
75
|
+
|
76
|
+
## Author
|
77
|
+
|
78
|
+
Ryan Carver (@rcarver / ryan@ryancarver.com)
|
79
|
+
|
80
|
+
## License
|
81
|
+
|
82
|
+
Copyright © 2012 Ryan Carver. Licensed under Ruby/MIT, see LICENSE.
|
data/Rakefile
CHANGED
@@ -1,14 +1,36 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
1
|
+
require 'bundler'
|
2
|
+
Bundler::GemHelper.install_tasks
|
3
|
+
|
4
|
+
require 'rake/testtask'
|
5
|
+
|
6
|
+
task :test => [:test_default, :test_ar]
|
7
|
+
|
8
|
+
Rake::TestTask.new(:test_default) do |t|
|
9
|
+
t.libs.push "lib", "test"
|
10
|
+
t.pattern = 'test/mysql_inspector/**/*_test.rb'
|
11
|
+
end
|
12
|
+
|
13
|
+
Rake::TestTask.new(:test_ar) do |t|
|
14
|
+
t.libs.push "lib", "test"
|
15
|
+
t.pattern = 'test/mysql_inspector_ar/**/*_test.rb'
|
16
|
+
end
|
17
|
+
|
18
|
+
|
19
|
+
def load_schema(name)
|
20
|
+
$LOAD_PATH.unshift "test"
|
21
|
+
require 'helpers/mysql_schemas'
|
22
|
+
require 'helpers/mysql_utils'
|
23
|
+
|
24
|
+
schemas = Object.new
|
25
|
+
schemas.extend MysqlSchemas
|
26
|
+
|
27
|
+
MysqlUtils.create_mysql_database("mysql_inspector_development", schemas.send(name))
|
28
|
+
end
|
29
|
+
|
30
|
+
task :db1 do
|
31
|
+
load_schema(:schema_a)
|
32
|
+
end
|
33
|
+
|
34
|
+
task :db2 do
|
35
|
+
load_schema(:schema_b)
|
36
|
+
end
|
data/bin/mysql-inspector
CHANGED
@@ -1,94 +1,18 @@
|
|
1
1
|
#!/usr/bin/env ruby
|
2
2
|
|
3
3
|
$LOAD_PATH.unshift File.dirname(__FILE__) + '/../lib'
|
4
|
-
require 'optparse'
|
5
|
-
require "mysql-inspector"
|
6
4
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
:grep => false,
|
12
|
-
:write => false,
|
13
|
-
:clean => false
|
14
|
-
}
|
15
|
-
|
16
|
-
OptionParser.new do |opts|
|
17
|
-
opts.banner = "Usage: #{File.basename $0} [options]"
|
18
|
-
|
19
|
-
opts.on("-c", "--current", "Perform the given action(s) with the current version.") do
|
20
|
-
options[:versions] << "current"
|
21
|
-
end
|
22
|
-
|
23
|
-
opts.on("-t", "--target", "Perform the given action(s) with the target version.") do
|
24
|
-
options[:versions] << "target"
|
25
|
-
end
|
26
|
-
|
27
|
-
opts.on("-w", "--write DB", "Store a schema for the database") do |db_name|
|
28
|
-
options[:write] = db_name
|
29
|
-
end
|
30
|
-
|
31
|
-
opts.on("-f", "--force", "Overwrite a previously stored schema") do |ok|
|
32
|
-
options[:clean] = true
|
33
|
-
end
|
34
|
-
|
35
|
-
opts.on("-d", "--diff", "Output a comparison between the current and target schemas") do |info|
|
36
|
-
options[:diff] = true
|
37
|
-
end
|
38
|
-
|
39
|
-
opts.on("-g", "--grep foo_id,bar_id", "Find columns and/or indices matching the pattern") do |columns|
|
40
|
-
options[:grep] = columns.split(",").collect { |c| c.strip }
|
41
|
-
end
|
42
|
-
|
43
|
-
opts.on("--to DIR", "Where to store schemas. Defaults to '.'") do |dir|
|
44
|
-
options[:base_dir] = dir
|
45
|
-
end
|
46
|
-
|
47
|
-
opts.on("-h", "--help", "What you're looking at") do
|
48
|
-
puts opts
|
49
|
-
end
|
50
|
-
|
51
|
-
puts opts if ARGV.empty?
|
52
|
-
|
53
|
-
end.parse!
|
54
|
-
|
55
|
-
def how_to_write_version(input)
|
56
|
-
"#{File.basename $0} --#{input.version} --write DB_NAME"
|
5
|
+
# Detect a Rails project.
|
6
|
+
rails_env = File.expand_path('config/environment', Dir.pwd)
|
7
|
+
if File.exist?(rails_env + ".rb")
|
8
|
+
ARGV.unshift("--rails")
|
57
9
|
end
|
58
10
|
|
59
|
-
|
11
|
+
require "mysql_inspector"
|
60
12
|
|
61
|
-
|
62
|
-
MysqlInspector::Dump.new(version, options[:base_dir])
|
63
|
-
end
|
13
|
+
config = MysqlInspector::Config.new
|
64
14
|
|
65
|
-
|
66
|
-
|
67
|
-
end
|
15
|
+
cli = MysqlInspector::CLI.new(config, $stdout, $stderr)
|
16
|
+
cli.run!(ARGV)
|
68
17
|
|
69
|
-
|
70
|
-
raise MysqlInspector::Precondition, "Please specify which version to write" if options[:versions].size == 0
|
71
|
-
raise MysqlInspector::Precondition, "I can only write one version at a time" unless options[:versions].size == 1
|
72
|
-
version = options[:versions].first
|
73
|
-
version.dump!(options[:write])
|
74
|
-
end
|
75
|
-
|
76
|
-
if options[:grep]
|
77
|
-
options[:versions].each do |v|
|
78
|
-
grep = MysqlInspector::Grep.new(v)
|
79
|
-
grep.find(STDOUT, *options[:grep])
|
80
|
-
end
|
81
|
-
end
|
82
|
-
|
83
|
-
if options[:diff]
|
84
|
-
inputs = ["current", "target"].collect { |version| MysqlInspector::Dump.new(version, options[:base_dir]) }
|
85
|
-
inputs.each do |input|
|
86
|
-
raise MysqlInspector::Precondition, "No #{input.version} version exists. Write one with `#{how_to_write_version(input)}`" unless input.exists?
|
87
|
-
end
|
88
|
-
comparison = MysqlInspector::Comparison.new(*inputs)
|
89
|
-
comparison.compare(STDOUT)
|
90
|
-
end
|
91
|
-
|
92
|
-
rescue MysqlInspector::Precondition => e
|
93
|
-
puts "#{e.message}."
|
94
|
-
end
|
18
|
+
exit cli.status
|
data/lib/mysql-inspector.rb
CHANGED
@@ -1,226 +1 @@
|
|
1
|
-
require
|
2
|
-
|
3
|
-
module MysqlInspector
|
4
|
-
|
5
|
-
Precondition = Class.new(StandardError)
|
6
|
-
|
7
|
-
module Utils
|
8
|
-
|
9
|
-
def file_to_table(file)
|
10
|
-
file[/(.*)\.sql/, 1]
|
11
|
-
end
|
12
|
-
|
13
|
-
def sanitize_schema!(schema)
|
14
|
-
schema.collect! { |line| line.rstrip[/(.*?),?$/, 1] }
|
15
|
-
schema.delete_if { |line| line =~ /(\/\*|--|CREATE TABLE)/ or line == ");" or line.strip.empty? }
|
16
|
-
schema.sort!
|
17
|
-
schema
|
18
|
-
end
|
19
|
-
end
|
20
|
-
|
21
|
-
module Config
|
22
|
-
extend self
|
23
|
-
extend Utils
|
24
|
-
|
25
|
-
def mysqldump(*args)
|
26
|
-
all_args = ["-u #{mysql_user}"] + args
|
27
|
-
Command.new(mysqldump_path, *all_args)
|
28
|
-
end
|
29
|
-
|
30
|
-
def strip_noise_from_dump!(dir)
|
31
|
-
Dir[File.join(dir, "*.sql")].each do |file|
|
32
|
-
lines = sanitize_schema!(File.readlines(file))
|
33
|
-
File.open(file, "w") do |f|
|
34
|
-
f.puts lines.join("\n")
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
def mysql_user
|
40
|
-
@mysql_user ||= "root"
|
41
|
-
end
|
42
|
-
|
43
|
-
def mysqldump_path
|
44
|
-
@mysqldump_path ||= begin
|
45
|
-
path = `which mysqldump`.chomp
|
46
|
-
raise Precondition, "mysqldump was not in your path" if path.empty?
|
47
|
-
path
|
48
|
-
end
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
class Command
|
53
|
-
def initialize(path, *args)
|
54
|
-
@path = path
|
55
|
-
@args = args
|
56
|
-
end
|
57
|
-
def to_s
|
58
|
-
"#{@path} #{@args * " "}"
|
59
|
-
end
|
60
|
-
def run!
|
61
|
-
system to_s
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
class Dump
|
66
|
-
def initialize(version, base_dir)
|
67
|
-
@version = version
|
68
|
-
@base_dir = base_dir
|
69
|
-
# TODO: sanity check base_dir for either a relative dir or /tmp/...
|
70
|
-
end
|
71
|
-
|
72
|
-
attr_reader :version, :base_dir
|
73
|
-
|
74
|
-
def db_date
|
75
|
-
@db_date ||= read_db_date
|
76
|
-
end
|
77
|
-
|
78
|
-
def dir
|
79
|
-
File.join(base_dir, version)
|
80
|
-
end
|
81
|
-
|
82
|
-
def clean!
|
83
|
-
FileUtils.rm_rf(dir)
|
84
|
-
end
|
85
|
-
|
86
|
-
def exists?
|
87
|
-
File.exist?(dir)
|
88
|
-
end
|
89
|
-
|
90
|
-
def dump!(db_name)
|
91
|
-
raise Precondition, "Can't overwrite an existing schema at #{dir.inspect}" if exists?
|
92
|
-
FileUtils.mkdir_p(dir)
|
93
|
-
Config.mysqldump("--no-data", "-T #{dir}", "--skip-opt", db_name).run!
|
94
|
-
Config.strip_noise_from_dump!(dir)
|
95
|
-
File.open(info_file, "w") { |f| f.puts(Time.now.utc.strftime("%Y-%m-%d")) }
|
96
|
-
end
|
97
|
-
|
98
|
-
protected
|
99
|
-
|
100
|
-
def info_file
|
101
|
-
File.join(dir, ".info")
|
102
|
-
end
|
103
|
-
|
104
|
-
def read_db_date
|
105
|
-
raise Precondition, "No dump exists at #{dir.inspect}" unless File.exist?(info_file)
|
106
|
-
File.read(info_file).strip
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
class Grep
|
111
|
-
include Utils
|
112
|
-
|
113
|
-
def initialize(dump)
|
114
|
-
@dump = dump
|
115
|
-
end
|
116
|
-
|
117
|
-
attr_reader :dump
|
118
|
-
|
119
|
-
def find(writer, *matchers)
|
120
|
-
writer.puts
|
121
|
-
writer.puts "Searching #{dump.version} (#{dump.db_date}) for #{matchers.inspect}"
|
122
|
-
writer.puts
|
123
|
-
files = Dir[File.join(dump.dir, "*.sql")].collect { |f| File.basename(f) }.sort
|
124
|
-
files.each do |f|
|
125
|
-
schema = File.read(File.join(dump.dir, f)).split("\n")
|
126
|
-
sanitize_schema!(schema)
|
127
|
-
|
128
|
-
matches = schema.select do |line|
|
129
|
-
matchers.all? do |matcher|
|
130
|
-
col, *items = matcher.split(/\s+/)
|
131
|
-
col = "`#{col}`"
|
132
|
-
[col, items].flatten.all? { |item| line.downcase =~ /#{Regexp.escape item.downcase}/ }
|
133
|
-
end
|
134
|
-
end
|
135
|
-
|
136
|
-
if matches.any?
|
137
|
-
writer.puts
|
138
|
-
writer.puts file_to_table(f)
|
139
|
-
writer.puts "*" * file_to_table(f).size
|
140
|
-
writer.puts
|
141
|
-
writer.puts "Found matching:"
|
142
|
-
writer.puts matches.join("\n")
|
143
|
-
writer.puts
|
144
|
-
writer.puts "Full schema:"
|
145
|
-
writer.puts schema.join("\n")
|
146
|
-
writer.puts
|
147
|
-
end
|
148
|
-
end
|
149
|
-
end
|
150
|
-
end
|
151
|
-
|
152
|
-
class Comparison
|
153
|
-
include Utils
|
154
|
-
|
155
|
-
def initialize(current, target)
|
156
|
-
@current = current
|
157
|
-
@target = target
|
158
|
-
end
|
159
|
-
|
160
|
-
attr_reader :current, :target
|
161
|
-
|
162
|
-
def ignore_files
|
163
|
-
["migration_info.sql"]
|
164
|
-
end
|
165
|
-
|
166
|
-
def compare(writer=STDOUT)
|
167
|
-
writer.puts
|
168
|
-
writer.puts "Current: #{current.version} (#{current.db_date})"
|
169
|
-
writer.puts "Target: #{target.version} (#{target.db_date})"
|
170
|
-
|
171
|
-
current_files = Dir[File.join(current.dir, "*.sql")].collect { |f| File.basename(f) }.sort
|
172
|
-
target_files = Dir[File.join(target.dir, "*.sql")].collect { |f| File.basename(f) }.sort
|
173
|
-
|
174
|
-
# Ignore some tables
|
175
|
-
current_files -= ignore_files
|
176
|
-
target_files -= ignore_files
|
177
|
-
|
178
|
-
files_only_in_target = target_files - current_files
|
179
|
-
files_only_in_current = current_files - target_files
|
180
|
-
common_files = target_files & current_files
|
181
|
-
|
182
|
-
if files_only_in_current.any?
|
183
|
-
writer.puts
|
184
|
-
writer.puts "Tables only in current"
|
185
|
-
writer.puts files_only_in_current.collect { |f| file_to_table(f) }.join(", ")
|
186
|
-
end
|
187
|
-
|
188
|
-
if files_only_in_target.any?
|
189
|
-
writer.puts
|
190
|
-
writer.puts "Tables in target but not in current"
|
191
|
-
writer.puts files_only_in_target.collect { |f| file_to_table(f) }.join(", ")
|
192
|
-
end
|
193
|
-
|
194
|
-
common_files.each do |f|
|
195
|
-
current_schema = File.read(File.join(current.dir, f)).split("\n")
|
196
|
-
target_schema = File.read(File.join(target.dir, f)).split("\n")
|
197
|
-
|
198
|
-
sanitize_schema!(current_schema)
|
199
|
-
sanitize_schema!(target_schema)
|
200
|
-
|
201
|
-
next if current_schema == target_schema
|
202
|
-
|
203
|
-
writer.puts
|
204
|
-
writer.puts file_to_table(f)
|
205
|
-
writer.puts "*" * file_to_table(f).size
|
206
|
-
writer.puts
|
207
|
-
|
208
|
-
only_in_target = target_schema - current_schema
|
209
|
-
only_in_current = current_schema - target_schema
|
210
|
-
|
211
|
-
if only_in_current.any?
|
212
|
-
writer.puts "only in current"
|
213
|
-
writer.puts only_in_current.join("\n")
|
214
|
-
writer.puts
|
215
|
-
end
|
216
|
-
if only_in_target.any?
|
217
|
-
writer.puts "only in target"
|
218
|
-
writer.puts only_in_target.join("\n")
|
219
|
-
writer.puts
|
220
|
-
end
|
221
|
-
end
|
222
|
-
end
|
223
|
-
|
224
|
-
end
|
225
|
-
|
226
|
-
end
|
1
|
+
require 'mysql_inspector'
|