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
@@ -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
|
+
|