mysql-inspector 0.0.6 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. data/.gitignore +5 -3
  2. data/CHANGELOG.md +8 -0
  3. data/Gemfile +6 -0
  4. data/LICENSE +20 -0
  5. data/README.md +82 -0
  6. data/Rakefile +36 -14
  7. data/bin/mysql-inspector +9 -85
  8. data/lib/mysql-inspector.rb +1 -226
  9. data/lib/mysql_inspector.rb +30 -0
  10. data/lib/mysql_inspector/access.rb +64 -0
  11. data/lib/mysql_inspector/ar/access.rb +55 -0
  12. data/lib/mysql_inspector/cli.rb +291 -0
  13. data/lib/mysql_inspector/column.rb +21 -0
  14. data/lib/mysql_inspector/config.rb +82 -0
  15. data/lib/mysql_inspector/constraint.rb +28 -0
  16. data/lib/mysql_inspector/diff.rb +82 -0
  17. data/lib/mysql_inspector/dump.rb +70 -0
  18. data/lib/mysql_inspector/grep.rb +65 -0
  19. data/lib/mysql_inspector/index.rb +25 -0
  20. data/lib/mysql_inspector/migrations.rb +37 -0
  21. data/lib/mysql_inspector/railtie.rb +17 -0
  22. data/lib/mysql_inspector/railties/databases.rake +92 -0
  23. data/lib/mysql_inspector/table.rb +147 -0
  24. data/lib/mysql_inspector/table_part.rb +21 -0
  25. data/lib/mysql_inspector/version.rb +3 -0
  26. data/mysql-inspector.gemspec +17 -36
  27. data/test/fixtures/migrate/111_create_users.rb +7 -0
  28. data/test/fixtures/migrate/222_create_things.rb +9 -0
  29. data/test/helper.rb +125 -0
  30. data/test/helper_ar.rb +37 -0
  31. data/test/helpers/mysql_schemas.rb +82 -0
  32. data/test/helpers/mysql_utils.rb +35 -0
  33. data/test/helpers/string_unindented.rb +13 -0
  34. data/test/mysql_inspector/cli_basics_test.rb +77 -0
  35. data/test/mysql_inspector/cli_diff_test.rb +60 -0
  36. data/test/mysql_inspector/cli_grep_test.rb +74 -0
  37. data/test/mysql_inspector/cli_load_test.rb +43 -0
  38. data/test/mysql_inspector/cli_write_test.rb +58 -0
  39. data/test/mysql_inspector/config_test.rb +14 -0
  40. data/test/mysql_inspector/diff_test.rb +82 -0
  41. data/test/mysql_inspector/dump_test.rb +81 -0
  42. data/test/mysql_inspector/grep_test.rb +61 -0
  43. data/test/mysql_inspector/table_test.rb +123 -0
  44. data/test/mysql_inspector_ar/ar_dump_test.rb +29 -0
  45. data/test/mysql_inspector_ar/ar_migrations_test.rb +47 -0
  46. metadata +123 -49
  47. data/README +0 -48
  48. data/VERSION +0 -1
data/.gitignore CHANGED
@@ -1,3 +1,5 @@
1
- current
2
- target
3
- pkg
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ tmp
@@ -0,0 +1,8 @@
1
+
2
+ 0.0.9 / 2013-05-26
3
+ ==================
4
+
5
+ * Total rewrite:
6
+ * Custom storage format
7
+ * Better CLI interface
8
+ * Can act as a Railtie to replace ActiveRecord schema management
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "http://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem "activerecord", ">=3.2", :require => false
6
+ gem "mysql2", :require => false
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.
@@ -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
- begin
2
- require 'jeweler'
3
- Jeweler::Tasks.new do |gemspec|
4
- gemspec.name = "mysql-inspector"
5
- gemspec.summary = "Tools for identifying changes to a MySQL schema"
6
- gemspec.email = "ryan@fivesevensix.com"
7
- gemspec.homepage = "http://github.com/rcarver/mysql-inspector"
8
- gemspec.authors = ["Ryan Carver"]
9
- gemspec.executables = ["mysql-inspector"]
10
- end
11
- Jeweler::GemcutterTasks.new
12
- rescue LoadError
13
- puts "Jeweler not available. Install it with: gem install jeweler"
14
- end
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
@@ -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
- options = {
8
- :versions => [],
9
- :base_dir => ".",
10
- :diff => false,
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
- begin
11
+ require "mysql_inspector"
60
12
 
61
- options[:versions].collect! do |version|
62
- MysqlInspector::Dump.new(version, options[:base_dir])
63
- end
13
+ config = MysqlInspector::Config.new
64
14
 
65
- if options[:clean]
66
- options[:versions].each { |v| v.clean! }
67
- end
15
+ cli = MysqlInspector::CLI.new(config, $stdout, $stderr)
16
+ cli.run!(ARGV)
68
17
 
69
- if options[:write]
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
@@ -1,226 +1 @@
1
- require "fileutils"
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'