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.
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'