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.
- 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
@@ -0,0 +1,147 @@
|
|
1
|
+
module MysqlInspector
|
2
|
+
class Table
|
3
|
+
|
4
|
+
BACKTICK_WORD = /`([^`]+)`/
|
5
|
+
BACKTICK_CSV = /\(([^\)]+)\)/
|
6
|
+
REFERENCE_OPTION = /RESTRICT|CASCADE|SET NULL|NO ACTION/
|
7
|
+
|
8
|
+
def initialize(schema)
|
9
|
+
@schema = schema
|
10
|
+
@lines = schema.split("\n")
|
11
|
+
@lines.delete_if { |line| line =~ /(\/\*|--)/ or line.strip.empty? }
|
12
|
+
end
|
13
|
+
|
14
|
+
# Public: Get then name of the table.
|
15
|
+
#
|
16
|
+
# Returns a String.
|
17
|
+
def table_name
|
18
|
+
@table_name ||= begin
|
19
|
+
line = @lines.find { |line| line =~ /CREATE TABLE #{BACKTICK_WORD}/}
|
20
|
+
$1 if line
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Public: Get all of the columns defined in the table.
|
25
|
+
#
|
26
|
+
# Returns an Array of MysqlInspector::Column.
|
27
|
+
def columns
|
28
|
+
@columns ||= @lines.map { |line|
|
29
|
+
if line.strip =~ /^#{BACKTICK_WORD} ([\w\(\)\d]+)/
|
30
|
+
name = $1
|
31
|
+
sql_type = $2
|
32
|
+
nullable = !!(line !~ /NOT NULL/)
|
33
|
+
default = line[/DEFAULT ('?[^']+'?)/, 1]
|
34
|
+
default = nil if default =~ /NULL/
|
35
|
+
auto_increment = !!(line =~ /AUTO_INCREMENT/)
|
36
|
+
table_part line, MysqlInspector::Column.new(name, sql_type, nullable, default, auto_increment)
|
37
|
+
end
|
38
|
+
}.compact.sort
|
39
|
+
end
|
40
|
+
|
41
|
+
# Public: Get all of the indices defined in the table
|
42
|
+
#
|
43
|
+
# Returns an Array of MysqlInspector::Index.
|
44
|
+
def indices
|
45
|
+
@indices ||= @lines.map { |line|
|
46
|
+
if line.strip =~ /^(UNIQUE )?KEY #{BACKTICK_WORD} #{BACKTICK_CSV}/
|
47
|
+
unique = !!$1
|
48
|
+
name = $2
|
49
|
+
column_names = backtick_names_in_csv($3)
|
50
|
+
table_part line, MysqlInspector::Index.new(name, column_names, unique)
|
51
|
+
elsif line.strip =~ /^PRIMARY KEY #{BACKTICK_CSV}/
|
52
|
+
unique = true
|
53
|
+
name = "PRIMARY KEY"
|
54
|
+
column_names = backtick_names_in_csv($1)
|
55
|
+
table_part line, MysqlInspector::Index.new(name, column_names, unique)
|
56
|
+
end
|
57
|
+
}.compact.sort
|
58
|
+
end
|
59
|
+
|
60
|
+
# Public: Get all of the constraints defined in the table
|
61
|
+
#
|
62
|
+
# Returns an Array of MysqlInspector::Constraint.
|
63
|
+
def constraints
|
64
|
+
@constraints ||= @lines.map { |line|
|
65
|
+
if line.strip =~ /^CONSTRAINT #{BACKTICK_WORD} FOREIGN KEY #{BACKTICK_CSV} REFERENCES #{BACKTICK_WORD} #{BACKTICK_CSV} ON DELETE (#{REFERENCE_OPTION}) ON UPDATE (#{REFERENCE_OPTION})$/
|
66
|
+
name = $1
|
67
|
+
column_names = backtick_names_in_csv($2)
|
68
|
+
foreign_name = $3
|
69
|
+
foreign_column_names = backtick_names_in_csv($4)
|
70
|
+
on_delete = $5
|
71
|
+
on_update = $6
|
72
|
+
table_part line, MysqlInspector::Constraint.new(name, column_names, foreign_name, foreign_column_names, on_update, on_delete)
|
73
|
+
end
|
74
|
+
}.compact.sort
|
75
|
+
end
|
76
|
+
|
77
|
+
def options
|
78
|
+
@options ||= begin
|
79
|
+
if line = @lines.find { |line| line =~ /ENGINE=/}
|
80
|
+
# AUTO_INCREMENT is not useful.
|
81
|
+
line.sub!(/AUTO_INCREMENT=\d+/, '')
|
82
|
+
# Compact multiple spaces.
|
83
|
+
line.gsub!(/\s+/, ' ')
|
84
|
+
# Remove paren at the beginning.
|
85
|
+
line.sub!(/^\)\s*/, '')
|
86
|
+
# Remove semicolon at the end.
|
87
|
+
line.chomp!(';')
|
88
|
+
line
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def eql?(other)
|
94
|
+
table_name == other.table_name &&
|
95
|
+
columns == other.columns &&
|
96
|
+
indices == other.indices &&
|
97
|
+
constraints == other.constraints &&
|
98
|
+
options = other.options
|
99
|
+
end
|
100
|
+
|
101
|
+
alias == eql?
|
102
|
+
|
103
|
+
def <=>(other)
|
104
|
+
table_name <=> other.table_name
|
105
|
+
end
|
106
|
+
|
107
|
+
def to_simple_schema
|
108
|
+
lines = []
|
109
|
+
|
110
|
+
lines << "CREATE TABLE `#{table_name}`"
|
111
|
+
lines << nil
|
112
|
+
simple_schema_items(lines, columns)
|
113
|
+
simple_schema_items(lines, indices)
|
114
|
+
simple_schema_items(lines, constraints)
|
115
|
+
lines << options
|
116
|
+
|
117
|
+
lines.join("\n")
|
118
|
+
end
|
119
|
+
|
120
|
+
def to_sql
|
121
|
+
lines = []
|
122
|
+
|
123
|
+
lines << "CREATE TABLE `#{table_name}` ("
|
124
|
+
lines << (columns + indices + constraints).map { |x| " #{x.to_sql}" }.join(",\n")
|
125
|
+
lines << ") #{options}"
|
126
|
+
|
127
|
+
lines.join("\n")
|
128
|
+
end
|
129
|
+
|
130
|
+
protected
|
131
|
+
|
132
|
+
def simple_schema_items(lines, items)
|
133
|
+
lines.concat items.map { |item| item.to_sql }
|
134
|
+
lines << nil if items.any?
|
135
|
+
end
|
136
|
+
|
137
|
+
def table_part(line, part)
|
138
|
+
part.table = self
|
139
|
+
part
|
140
|
+
end
|
141
|
+
|
142
|
+
def backtick_names_in_csv(string)
|
143
|
+
string.split(',').map { |x| x[BACKTICK_WORD, 1] }
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
147
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
module MysqlInspector
|
2
|
+
module TablePart
|
3
|
+
|
4
|
+
attr_accessor :table
|
5
|
+
|
6
|
+
def <=>(other)
|
7
|
+
name <=> other.name
|
8
|
+
end
|
9
|
+
|
10
|
+
protected
|
11
|
+
|
12
|
+
def quote(word)
|
13
|
+
"`#{word}`"
|
14
|
+
end
|
15
|
+
|
16
|
+
def paren(words)
|
17
|
+
"(#{words * ","})"
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
data/mysql-inspector.gemspec
CHANGED
@@ -1,44 +1,25 @@
|
|
1
|
-
# Generated by jeweler
|
2
|
-
# DO NOT EDIT THIS FILE DIRECTLY
|
3
|
-
# Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
|
4
1
|
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "mysql_inspector/version"
|
5
4
|
|
6
5
|
Gem::Specification.new do |s|
|
7
|
-
s.name
|
8
|
-
s.version
|
6
|
+
s.name = "mysql-inspector"
|
7
|
+
s.version = MysqlInspector::VERSION
|
8
|
+
s.authors = ["Ryan Carver"]
|
9
|
+
s.email = ["ryan@ryancarver.com"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{Store and understand your MySQL schema}
|
12
|
+
s.description = %q{Store and understand your MySQL schema}
|
9
13
|
|
10
|
-
s.
|
11
|
-
|
12
|
-
s.
|
13
|
-
s.
|
14
|
-
s.
|
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"]
|
14
|
+
s.rubyforge_project = "mysql-inspector"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
30
19
|
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
20
|
|
34
|
-
|
35
|
-
|
36
|
-
s.specification_version = 3
|
21
|
+
s.add_development_dependency "rake"
|
22
|
+
s.add_development_dependency "minitest"
|
37
23
|
|
38
|
-
|
39
|
-
else
|
40
|
-
end
|
41
|
-
else
|
42
|
-
end
|
24
|
+
# s.add_runtime_dependency "rest-client"
|
43
25
|
end
|
44
|
-
|
data/test/helper.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'minitest/autorun'
|
2
|
+
require 'minitest/mock'
|
3
|
+
require 'open3'
|
4
|
+
require 'ostruct'
|
5
|
+
require 'stringio'
|
6
|
+
|
7
|
+
require 'mysql_inspector'
|
8
|
+
|
9
|
+
require 'helpers/mysql_utils'
|
10
|
+
require 'helpers/mysql_schemas'
|
11
|
+
require 'helpers/string_unindented'
|
12
|
+
|
13
|
+
class MysqlInspectorSpec < MiniTest::Spec
|
14
|
+
include MysqlSchemas
|
15
|
+
|
16
|
+
def it msg
|
17
|
+
raise "A block must not be passed to the example-level +it+" if block_given?
|
18
|
+
@__current_msg = "it #{msg}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def message msg = nil, ending = ".", &default
|
22
|
+
super(msg || @__current_msg, ending, &default)
|
23
|
+
end
|
24
|
+
|
25
|
+
register_spec_type(self) { |desc| true }
|
26
|
+
|
27
|
+
# Create a temporary directory. This directory will exist for the life of
|
28
|
+
# the spec.
|
29
|
+
#
|
30
|
+
# id - Identifier of the tmpdir (default: the default identifier).
|
31
|
+
#
|
32
|
+
# Returns a String.
|
33
|
+
def tmpdir(id=:default)
|
34
|
+
@tmpdirs[id] ||= Dir.mktmpdir
|
35
|
+
end
|
36
|
+
|
37
|
+
# Get the name of the test database.
|
38
|
+
#
|
39
|
+
# Returns a String.
|
40
|
+
def database_name
|
41
|
+
"mysql_inspector_test"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Create a test mysql database. The database will exist for the life
|
45
|
+
# of the spec.
|
46
|
+
#
|
47
|
+
# schema - String schema to create (default: no schema).
|
48
|
+
#
|
49
|
+
# Returns nothing.
|
50
|
+
def create_mysql_database(schema="")
|
51
|
+
@mysql_database = true
|
52
|
+
MysqlUtils.create_mysql_database(database_name, schema)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Drop the test mysql database.
|
56
|
+
#
|
57
|
+
# Returns nothing.
|
58
|
+
def drop_mysql_database
|
59
|
+
MysqlUtils.drop_mysql_database(database_name)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Get access to the mysql database.
|
63
|
+
#
|
64
|
+
# Returns a MysqlInspector:Access.
|
65
|
+
def access
|
66
|
+
(@access ||= {})[database_name] ||= MysqlInspector::Access.new(database_name, "root", nil, "mysql")
|
67
|
+
end
|
68
|
+
|
69
|
+
let(:config) { MysqlInspector::Config.new }
|
70
|
+
|
71
|
+
before do
|
72
|
+
@tmpdirs = {}
|
73
|
+
@mysql_database = nil
|
74
|
+
end
|
75
|
+
|
76
|
+
after do
|
77
|
+
@tmpdirs.values.each { |dir| FileUtils.rm_rf dir }
|
78
|
+
drop_mysql_database if @mysql_database
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
class MysqlInspectorCliSpec < MysqlInspectorSpec
|
83
|
+
|
84
|
+
register_spec_type(self) { |desc| desc =~ /mysql-inspector/ }
|
85
|
+
|
86
|
+
before do
|
87
|
+
config.dir = tmpdir
|
88
|
+
end
|
89
|
+
|
90
|
+
def parse_command(klass, argv)
|
91
|
+
command = klass.new(config, StringIO.new, StringIO.new)
|
92
|
+
command.parse!(argv)
|
93
|
+
command
|
94
|
+
end
|
95
|
+
|
96
|
+
def run_command(klass, argv)
|
97
|
+
command = klass.new(config, StringIO.new, StringIO.new)
|
98
|
+
command.parse!(argv)
|
99
|
+
command.run!
|
100
|
+
command
|
101
|
+
end
|
102
|
+
|
103
|
+
def mysql_inspector(args)
|
104
|
+
cli = MysqlInspector::CLI.new(config, StringIO.new, StringIO.new)
|
105
|
+
argv = args.split(/\s+/).map { |x| x.gsub(/'/, '') }
|
106
|
+
cli.run!(argv)
|
107
|
+
cli
|
108
|
+
end
|
109
|
+
|
110
|
+
def inspect_database(args)
|
111
|
+
mysql_inspector args
|
112
|
+
end
|
113
|
+
|
114
|
+
def stdout
|
115
|
+
subject.stdout.string.chomp
|
116
|
+
end
|
117
|
+
|
118
|
+
def stderr
|
119
|
+
subject.stderr.string.chomp
|
120
|
+
end
|
121
|
+
|
122
|
+
def status
|
123
|
+
subject.status
|
124
|
+
end
|
125
|
+
end
|
data/test/helper_ar.rb
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'active_record'
|
2
|
+
require 'mysql2'
|
3
|
+
require 'helper'
|
4
|
+
|
5
|
+
class MysqlInspectorActiveRecordSpec < MysqlInspectorSpec
|
6
|
+
|
7
|
+
register_spec_type(self) { |desc| desc =~ /activerecord/ }
|
8
|
+
|
9
|
+
before do
|
10
|
+
unless ActiveRecord::Base.connected?
|
11
|
+
ActiveRecord::Base.establish_connection(
|
12
|
+
:adapter => :mysql2,
|
13
|
+
:database => database_name,
|
14
|
+
:username => "root",
|
15
|
+
:password => nil
|
16
|
+
)
|
17
|
+
end
|
18
|
+
create_mysql_database
|
19
|
+
config.migrations = true
|
20
|
+
end
|
21
|
+
|
22
|
+
# Execute all of the fixture migrations.
|
23
|
+
#
|
24
|
+
# Returns nothing.
|
25
|
+
def run_active_record_migrations!
|
26
|
+
ActiveRecord::Migration.verbose = false
|
27
|
+
ActiveRecord::Migrator.migrate(["test/fixtures/migrate"])
|
28
|
+
end
|
29
|
+
|
30
|
+
# Get access to the mysql database.
|
31
|
+
#
|
32
|
+
# Returns a MysqlInspector:AR::Access.
|
33
|
+
def access
|
34
|
+
MysqlInspector::AR::Access.new(ActiveRecord::Base.connection)
|
35
|
+
end
|
36
|
+
|
37
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'helpers/string_unindented'
|
2
|
+
|
3
|
+
# Sample table schemas used for testing.
|
4
|
+
module MysqlSchemas
|
5
|
+
|
6
|
+
# A sample starting database.
|
7
|
+
def schema_a
|
8
|
+
[ideas_schema, colors_schema, things_schema_1].join(";\n")
|
9
|
+
end
|
10
|
+
|
11
|
+
# A sample changed database.
|
12
|
+
def schema_b
|
13
|
+
[users_schema, ideas_schema, things_schema_2].join(";\n")
|
14
|
+
end
|
15
|
+
|
16
|
+
def colors_schema
|
17
|
+
<<-STR.unindented
|
18
|
+
CREATE TABLE `colors` (
|
19
|
+
`name` varchar(255) NOT NULL,
|
20
|
+
UNIQUE KEY `colors_primary` (`name`)
|
21
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
22
|
+
STR
|
23
|
+
end
|
24
|
+
|
25
|
+
def ideas_schema
|
26
|
+
<<-STR.unindented
|
27
|
+
CREATE TABLE `ideas` (
|
28
|
+
`id` int(11) NOT NULL AUTO_INCREMENT,
|
29
|
+
`name` varchar(255) NOT NULL,
|
30
|
+
`description` text NOT NULL,
|
31
|
+
PRIMARY KEY (`id`),
|
32
|
+
KEY `name` (`name`)
|
33
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
34
|
+
STR
|
35
|
+
end
|
36
|
+
|
37
|
+
def users_schema
|
38
|
+
<<-STR.unindented
|
39
|
+
CREATE TABLE `users` (
|
40
|
+
`id` int(11) NOT NULL AUTO_INCREMENT,
|
41
|
+
`first_name` varchar(255) NOT NULL,
|
42
|
+
`last_name` varchar(255) NOT NULL,
|
43
|
+
UNIQUE KEY `users_primary` (`id`),
|
44
|
+
KEY `name` (`first_name`,`last_name`)
|
45
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
46
|
+
STR
|
47
|
+
end
|
48
|
+
|
49
|
+
def things_schema_1
|
50
|
+
<<-STR.unindented
|
51
|
+
CREATE TABLE `things` (
|
52
|
+
`id` int(11) NOT NULL AUTO_INCREMENT,
|
53
|
+
`name` varchar(255) NOT NULL DEFAULT 'toy',
|
54
|
+
`weight` int(11) DEFAULT NULL,
|
55
|
+
`color` varchar(255) NOT NULL,
|
56
|
+
UNIQUE KEY `things_primary` (`id`),
|
57
|
+
KEY `color` (`color`),
|
58
|
+
CONSTRAINT `belongs_to_color` FOREIGN KEY (`color`) REFERENCES `colors` (`name`) ON DELETE NO ACTION ON UPDATE CASCADE
|
59
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
60
|
+
STR
|
61
|
+
end
|
62
|
+
|
63
|
+
def things_schema_2
|
64
|
+
<<-STR.unindented
|
65
|
+
CREATE TABLE `things` (
|
66
|
+
`id` int(11) NOT NULL AUTO_INCREMENT,
|
67
|
+
`name` varchar(255) NOT NULL DEFAULT 'toy',
|
68
|
+
`weight` int(11) DEFAULT NULL,
|
69
|
+
`first_name` varchar(255) NOT NULL,
|
70
|
+
`last_name` varchar(255) NOT NULL,
|
71
|
+
UNIQUE KEY `things_primary` (`id`),
|
72
|
+
KEY `name` (`first_name`,`last_name`),
|
73
|
+
CONSTRAINT `belongs_to_user` FOREIGN KEY (`first_name`, `last_name`) REFERENCES `users` (`first_name`, `last_name`) ON DELETE NO ACTION ON UPDATE CASCADE
|
74
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
|
75
|
+
STR
|
76
|
+
end
|
77
|
+
|
78
|
+
def things_schema
|
79
|
+
things_schema_2
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|