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,35 @@
|
|
1
|
+
require 'open3'
|
2
|
+
require 'tempfile'
|
3
|
+
|
4
|
+
module MysqlUtils
|
5
|
+
extend self
|
6
|
+
|
7
|
+
def create_mysql_database(database_name, schema)
|
8
|
+
drop_mysql_database(database_name)
|
9
|
+
syscall "echo 'CREATE DATABASE #{database_name}' | #{mysql_command}"
|
10
|
+
Tempfile.open('schema') do |file|
|
11
|
+
file.puts(schema)
|
12
|
+
file.flush
|
13
|
+
syscall "cat #{file.path} | #{mysql_command} #{database_name}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def drop_mysql_database(database_name)
|
18
|
+
syscall "echo 'DROP DATABASE IF EXISTS #{database_name}' | #{mysql_command}"
|
19
|
+
end
|
20
|
+
|
21
|
+
protected
|
22
|
+
|
23
|
+
def mysql_command
|
24
|
+
@mysql_command ||= begin
|
25
|
+
path = `which mysql`.chomp
|
26
|
+
raise "mysql is not in your PATH" if path.empty?
|
27
|
+
"#{path} -uroot"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def syscall(command)
|
32
|
+
out, err, status = Open3.capture3(command)
|
33
|
+
raise "FAILED:\n\nstdout:\n#{out}\n\nstderr:\n#{err}" unless status.exitstatus == 0
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
class String
|
2
|
+
# Strip left indentation from a string. Call this on a HEREDOC
|
3
|
+
# string to unindent it.
|
4
|
+
def unindented
|
5
|
+
lines = self.split("\n")
|
6
|
+
indent_level = (lines[0][/^(\s*)/, 1] || "").size
|
7
|
+
lines.map { |line|
|
8
|
+
line.sub(/^\s{#{indent_level}}/, '')
|
9
|
+
}.join("\n") + "\n"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
|
@@ -0,0 +1,77 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe "mysql-inspector -v" do
|
4
|
+
|
5
|
+
subject { mysql_inspector "-v" }
|
6
|
+
|
7
|
+
it "shows the version" do
|
8
|
+
stderr.must_equal ""
|
9
|
+
stdout.must_equal MysqlInspector::VERSION
|
10
|
+
status.must_equal 0
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "mysql-inspector -h" do
|
15
|
+
|
16
|
+
subject { mysql_inspector "-h" }
|
17
|
+
|
18
|
+
it "shows the help" do
|
19
|
+
stderr.must_equal ""
|
20
|
+
stdout.must_equal <<-EOL.unindented
|
21
|
+
Usage: mysql-inspector [options] command [command args]
|
22
|
+
|
23
|
+
Options
|
24
|
+
|
25
|
+
--out DIR Where to store schemas. Defaults to '.'
|
26
|
+
--rails Configure for a Rails project
|
27
|
+
-h, --help What you're looking at
|
28
|
+
-v, --version Show the version of mysql-inspector
|
29
|
+
|
30
|
+
Commands
|
31
|
+
|
32
|
+
write DATABASE [VERSION]
|
33
|
+
load DATABASE [VERSION]
|
34
|
+
diff
|
35
|
+
diff TO
|
36
|
+
diff FROM TO
|
37
|
+
grep PATTERN [PATTERN]
|
38
|
+
|
39
|
+
EOL
|
40
|
+
status.must_equal 0
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
describe "mysql-inspector an unknown command" do
|
45
|
+
|
46
|
+
subject { mysql_inspector "unknown_command" }
|
47
|
+
|
48
|
+
it "fails" do
|
49
|
+
stderr.must_equal "Unknown command \"unknown_command\""
|
50
|
+
stdout.must_equal ""
|
51
|
+
status.must_equal 1
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
describe "mysql-inspector error cases" do
|
56
|
+
|
57
|
+
describe "when the database does not exist" do
|
58
|
+
subject { inspect_database "write #{database_name}" }
|
59
|
+
it "fails" do
|
60
|
+
stdout.must_equal ""
|
61
|
+
stderr.must_equal "The database #{database_name} does not exist"
|
62
|
+
status.must_equal 1
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
describe "when the dump does not exist" do
|
67
|
+
subject { inspect_database "load #{database_name}" }
|
68
|
+
before do
|
69
|
+
create_mysql_database ""
|
70
|
+
end
|
71
|
+
it "fails" do
|
72
|
+
stdout.must_equal ""
|
73
|
+
stderr.must_equal "Dump \"current\" does not exist"
|
74
|
+
status.must_equal 1
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe "mysql-inspector diff" do
|
4
|
+
|
5
|
+
describe "parsing arguments" do
|
6
|
+
|
7
|
+
subject { parse_command(MysqlInspector::CLI::DiffCommand, args) }
|
8
|
+
let(:args) { [] }
|
9
|
+
|
10
|
+
specify "it compares current to target" do
|
11
|
+
subject.ivar(:version1).must_equal "current"
|
12
|
+
subject.ivar(:version2).must_equal "target"
|
13
|
+
end
|
14
|
+
|
15
|
+
specify "it compares current to something else" do
|
16
|
+
args << "other"
|
17
|
+
subject.ivar(:version1).must_equal "current"
|
18
|
+
subject.ivar(:version2).must_equal "other"
|
19
|
+
end
|
20
|
+
|
21
|
+
specify "it compares two arbitrary versions" do
|
22
|
+
args << "other1"
|
23
|
+
args << "other2"
|
24
|
+
subject.ivar(:version1).must_equal "other1"
|
25
|
+
subject.ivar(:version2).must_equal "other2"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "running" do
|
30
|
+
|
31
|
+
subject { inspect_database "diff" }
|
32
|
+
|
33
|
+
before do
|
34
|
+
create_mysql_database schema_a
|
35
|
+
inspect_database "write #{database_name} current"
|
36
|
+
create_mysql_database schema_b
|
37
|
+
inspect_database "write #{database_name} target"
|
38
|
+
end
|
39
|
+
|
40
|
+
specify do
|
41
|
+
stderr.must_equal ""
|
42
|
+
stdout.must_equal <<-EOL.unindented
|
43
|
+
diff current target
|
44
|
+
|
45
|
+
- colors
|
46
|
+
= things
|
47
|
+
COL - `color` varchar(255) NOT NULL
|
48
|
+
+ `first_name` varchar(255) NOT NULL
|
49
|
+
+ `last_name` varchar(255) NOT NULL
|
50
|
+
IDX - KEY `color` (`color`)
|
51
|
+
+ KEY `name` (`first_name`,`last_name`)
|
52
|
+
CST - CONSTRAINT `belongs_to_color` FOREIGN KEY (`color`) REFERENCES `colors` (`name`) ON DELETE NO ACTION ON UPDATE CASCADE
|
53
|
+
+ CONSTRAINT `belongs_to_user` FOREIGN KEY (`first_name`,`last_name`) REFERENCES `users` (`first_name`,`last_name`) ON DELETE NO ACTION ON UPDATE CASCADE
|
54
|
+
+ users
|
55
|
+
|
56
|
+
EOL
|
57
|
+
status.must_equal 0
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe "mysql-inspector grep" do
|
4
|
+
|
5
|
+
describe "parsing arguments" do
|
6
|
+
|
7
|
+
subject { parse_command(MysqlInspector::CLI::GrepCommand, args) }
|
8
|
+
let(:args) { [] }
|
9
|
+
|
10
|
+
specify "it searches current" do
|
11
|
+
args.concat ["a", "^b"]
|
12
|
+
subject.ivar(:version).must_equal "current"
|
13
|
+
subject.ivar(:matchers).must_equal [/a/, /^b/]
|
14
|
+
end
|
15
|
+
|
16
|
+
specify "it searches another version" do
|
17
|
+
skip "not supported. how would the args work?"
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "running" do
|
22
|
+
before do
|
23
|
+
create_mysql_database [users_schema, things_schema] * ";"
|
24
|
+
inspect_database "write #{database_name}"
|
25
|
+
end
|
26
|
+
|
27
|
+
subject { inspect_database "grep #{matchers * ' '}" }
|
28
|
+
let(:matchers) { [] }
|
29
|
+
|
30
|
+
specify "searching for a single term" do
|
31
|
+
matchers << "name"
|
32
|
+
|
33
|
+
stderr.must_equal ""
|
34
|
+
stdout.must_equal <<-EOL.unindented
|
35
|
+
grep /name/
|
36
|
+
|
37
|
+
things
|
38
|
+
COL `first_name` varchar(255) NOT NULL
|
39
|
+
`last_name` varchar(255) NOT NULL
|
40
|
+
`name` varchar(255) NOT NULL DEFAULT 'toy'
|
41
|
+
IDX KEY `name` (`first_name`,`last_name`)
|
42
|
+
CST CONSTRAINT `belongs_to_user` FOREIGN KEY (`first_name`,`last_name`) REFERENCES `users` (`first_name`,`last_name`) ON DELETE NO ACTION ON UPDATE CASCADE
|
43
|
+
|
44
|
+
users
|
45
|
+
COL `first_name` varchar(255) NOT NULL
|
46
|
+
`last_name` varchar(255) NOT NULL
|
47
|
+
IDX KEY `name` (`first_name`,`last_name`)
|
48
|
+
|
49
|
+
EOL
|
50
|
+
status.must_equal 0
|
51
|
+
end
|
52
|
+
|
53
|
+
specify "searching for multiple terms" do
|
54
|
+
matchers << "name"
|
55
|
+
matchers << "first"
|
56
|
+
|
57
|
+
stderr.must_equal ""
|
58
|
+
stdout.must_equal <<-EOL.unindented
|
59
|
+
grep /name/ AND /first/
|
60
|
+
|
61
|
+
things
|
62
|
+
COL `first_name` varchar(255) NOT NULL
|
63
|
+
IDX KEY `name` (`first_name`,`last_name`)
|
64
|
+
CST CONSTRAINT `belongs_to_user` FOREIGN KEY (`first_name`,`last_name`) REFERENCES `users` (`first_name`,`last_name`) ON DELETE NO ACTION ON UPDATE CASCADE
|
65
|
+
|
66
|
+
users
|
67
|
+
COL `first_name` varchar(255) NOT NULL
|
68
|
+
IDX KEY `name` (`first_name`,`last_name`)
|
69
|
+
|
70
|
+
EOL
|
71
|
+
status.must_equal 0
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
describe "mysql-inspector load" do
|
2
|
+
|
3
|
+
describe "parsing arguments" do
|
4
|
+
|
5
|
+
subject { parse_command(MysqlInspector::CLI::LoadCommand, args) }
|
6
|
+
let(:args) { [] }
|
7
|
+
|
8
|
+
specify "it fails when you don't specify a database" do
|
9
|
+
stderr.must_equal "Usage: mysql-inspector load DATABASE [VERSION]"
|
10
|
+
stdout.must_equal ""
|
11
|
+
end
|
12
|
+
|
13
|
+
specify "it loads from current" do
|
14
|
+
args.concat ["my_database"]
|
15
|
+
subject.ivar(:database).must_equal "my_database"
|
16
|
+
subject.ivar(:version).must_equal "current"
|
17
|
+
end
|
18
|
+
|
19
|
+
specify "it loads another version" do
|
20
|
+
args.concat ["my_database", "other"]
|
21
|
+
subject.ivar(:database).must_equal "my_database"
|
22
|
+
subject.ivar(:version).must_equal "other"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "running" do
|
27
|
+
subject { inspect_database "load #{database_name}" }
|
28
|
+
specify do
|
29
|
+
create_mysql_database schema_b
|
30
|
+
inspect_database "write #{database_name}"
|
31
|
+
create_mysql_database ideas_schema
|
32
|
+
access.table_names.size.must_equal 1
|
33
|
+
|
34
|
+
it "outputs nothing"
|
35
|
+
stdout.must_equal ""
|
36
|
+
stderr.must_equal ""
|
37
|
+
status.must_equal 0
|
38
|
+
|
39
|
+
it "creates all tables"
|
40
|
+
access.table_names.size.must_equal 3
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe "mysql-inspector write" do
|
4
|
+
|
5
|
+
describe "parsing arguments" do
|
6
|
+
|
7
|
+
subject { parse_command(MysqlInspector::CLI::WriteCommand, args) }
|
8
|
+
let(:args) { [] }
|
9
|
+
|
10
|
+
specify "it fails when you don't specify a database" do
|
11
|
+
stderr.must_equal "Usage: mysql-inspector write DATABASE [VERSION]"
|
12
|
+
stdout.must_equal ""
|
13
|
+
end
|
14
|
+
|
15
|
+
specify "it writes to current" do
|
16
|
+
args.concat ["my_database"]
|
17
|
+
subject.ivar(:database).must_equal "my_database"
|
18
|
+
subject.ivar(:version).must_equal "current"
|
19
|
+
end
|
20
|
+
|
21
|
+
specify "it writes to another version" do
|
22
|
+
args.concat ["my_database", "other"]
|
23
|
+
subject.ivar(:database).must_equal "my_database"
|
24
|
+
subject.ivar(:version).must_equal "other"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "running" do
|
29
|
+
|
30
|
+
subject { run_command(MysqlInspector::CLI::WriteCommand, args) }
|
31
|
+
let(:args) { [database_name] }
|
32
|
+
|
33
|
+
let(:dirname) { "#{tmpdir}/current" }
|
34
|
+
|
35
|
+
specify "when the database does not exist" do
|
36
|
+
it "fails"
|
37
|
+
stdout.must_equal ""
|
38
|
+
stderr.must_equal "The database mysql_inspector_test does not exist"
|
39
|
+
status.must_equal 1
|
40
|
+
|
41
|
+
it "does not create a directory"
|
42
|
+
File.directory?(dirname).must_equal false
|
43
|
+
end
|
44
|
+
|
45
|
+
specify "when the database exists" do
|
46
|
+
create_mysql_database schema_a
|
47
|
+
|
48
|
+
it "succeeds"
|
49
|
+
stdout.must_equal ""
|
50
|
+
stderr.must_equal ""
|
51
|
+
status.must_equal 0
|
52
|
+
|
53
|
+
it "creates a directory and files"
|
54
|
+
File.directory?(dirname).must_equal true
|
55
|
+
Dir.glob(dirname + "/*.table").size.must_equal 3
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe MysqlInspector::Config do
|
4
|
+
|
5
|
+
it "uses Access" do
|
6
|
+
config.database_name = "test"
|
7
|
+
config.access.must_be_instance_of MysqlInspector::Access
|
8
|
+
end
|
9
|
+
|
10
|
+
it "uses Dump" do
|
11
|
+
config.create_dump("test").must_be_instance_of MysqlInspector::Dump
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
describe MysqlInspector::Diff do
|
4
|
+
|
5
|
+
let(:current_dump) do
|
6
|
+
mock = MiniTest::Mock.new
|
7
|
+
mock.expect :tables, [
|
8
|
+
MysqlInspector::Table.new(ideas_schema),
|
9
|
+
MysqlInspector::Table.new(colors_schema),
|
10
|
+
MysqlInspector::Table.new(things_schema_1)
|
11
|
+
]
|
12
|
+
mock
|
13
|
+
end
|
14
|
+
|
15
|
+
let(:target_dump) do
|
16
|
+
mock = MiniTest::Mock.new
|
17
|
+
mock.expect :tables, [
|
18
|
+
MysqlInspector::Table.new(users_schema),
|
19
|
+
MysqlInspector::Table.new(ideas_schema),
|
20
|
+
MysqlInspector::Table.new(things_schema_2)
|
21
|
+
]
|
22
|
+
mock
|
23
|
+
end
|
24
|
+
|
25
|
+
subject do
|
26
|
+
MysqlInspector::Diff.new(current_dump, target_dump)
|
27
|
+
end
|
28
|
+
|
29
|
+
before do
|
30
|
+
subject.execute
|
31
|
+
end
|
32
|
+
|
33
|
+
def table_names(tables)
|
34
|
+
tables.map { |t| t.table_name }
|
35
|
+
end
|
36
|
+
|
37
|
+
def names(items)
|
38
|
+
items.map { |t| t.name }
|
39
|
+
end
|
40
|
+
|
41
|
+
specify "the types of tables" do
|
42
|
+
|
43
|
+
it "finds tables that were added"
|
44
|
+
table_names(subject.added_tables).must_equal ["users"]
|
45
|
+
|
46
|
+
it "finds tables that are missing"
|
47
|
+
table_names(subject.missing_tables).must_equal ["colors"]
|
48
|
+
|
49
|
+
it "finds tables that are equal"
|
50
|
+
table_names(subject.equal_tables).must_equal ["ideas"]
|
51
|
+
|
52
|
+
it "finds tables that differ"
|
53
|
+
table_names(subject.different_tables).must_equal ["things"]
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "a table that differs" do
|
57
|
+
|
58
|
+
let(:table) { subject.different_tables.first }
|
59
|
+
|
60
|
+
specify "the parts of the table" do
|
61
|
+
|
62
|
+
it "finds the columns that are added"
|
63
|
+
names(table.added_columns).must_equal ["first_name", "last_name"]
|
64
|
+
|
65
|
+
it "finds the columns that are missing"
|
66
|
+
names(table.missing_columns).must_equal ["color"]
|
67
|
+
|
68
|
+
it "finds the indices that are added"
|
69
|
+
names(table.added_indices).must_equal ["name"]
|
70
|
+
|
71
|
+
it "finds the indices that are missing"
|
72
|
+
names(table.missing_indices).must_equal ["color"]
|
73
|
+
|
74
|
+
it "finds the constraints that are added"
|
75
|
+
names(table.added_constraints).must_equal ["belongs_to_user"]
|
76
|
+
|
77
|
+
it "finds the constraints that are missing"
|
78
|
+
names(table.missing_constraints).must_equal ["belongs_to_color"]
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|