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,82 @@
|
|
1
|
+
module MysqlInspector
|
2
|
+
class Config
|
3
|
+
|
4
|
+
def initialize
|
5
|
+
@mysql_user = "root"
|
6
|
+
@mysql_password = nil
|
7
|
+
@mysql_binary = "mysql"
|
8
|
+
@dir = File.expand_path(Dir.pwd)
|
9
|
+
@migrations = false
|
10
|
+
@rails = false
|
11
|
+
end
|
12
|
+
|
13
|
+
#
|
14
|
+
# Config
|
15
|
+
#
|
16
|
+
|
17
|
+
attr_accessor :mysql_user
|
18
|
+
attr_accessor :mysql_password
|
19
|
+
attr_accessor :mysql_binary
|
20
|
+
|
21
|
+
attr_accessor :database_name
|
22
|
+
attr_accessor :dir
|
23
|
+
|
24
|
+
attr_accessor :migrations
|
25
|
+
attr_accessor :rails
|
26
|
+
|
27
|
+
def rails!
|
28
|
+
@rails = true
|
29
|
+
@migrations = true
|
30
|
+
@dir = "db"
|
31
|
+
end
|
32
|
+
|
33
|
+
#
|
34
|
+
# API
|
35
|
+
#
|
36
|
+
|
37
|
+
def create_dump(version)
|
38
|
+
raise ["Missing dir or version", dir, version].inspect if dir.nil? or version.nil?
|
39
|
+
file = File.join(dir, version)
|
40
|
+
extras = []
|
41
|
+
extras << Migrations.new(file) if migrations
|
42
|
+
Dump.new(file, *extras)
|
43
|
+
end
|
44
|
+
|
45
|
+
def write_dump(version)
|
46
|
+
create_dump(version).write!(access)
|
47
|
+
end
|
48
|
+
|
49
|
+
def load_dump(version)
|
50
|
+
create_dump(version).load!(access)
|
51
|
+
end
|
52
|
+
|
53
|
+
#
|
54
|
+
# Impl
|
55
|
+
#
|
56
|
+
|
57
|
+
def load_rails_env!
|
58
|
+
if rails
|
59
|
+
if !defined?(Rails)
|
60
|
+
rails_env = File.expand_path('config/environment', Dir.pwd)
|
61
|
+
if File.exist?(rails_env + ".rb")
|
62
|
+
require rails_env
|
63
|
+
end
|
64
|
+
end
|
65
|
+
if database_name
|
66
|
+
config = ActiveRecord::Base.configurations[database_name]
|
67
|
+
config or raise MysqlInspector::Access::Error, "The database configuration #{database_name.inspect} does not exist"
|
68
|
+
ActiveRecord::Base.establish_connection(config)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def access
|
74
|
+
load_rails_env!
|
75
|
+
if migrations
|
76
|
+
MysqlInspector::AR::Access.new(ActiveRecord::Base.connection)
|
77
|
+
else
|
78
|
+
MysqlInspector::Access.new(database_name, mysql_user, mysql_password, mysql_binary)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module MysqlInspector
|
2
|
+
class Constraint < Struct.new(:name, :column_names, :foreign_table, :foreign_column_names, :on_update, :on_delete)
|
3
|
+
|
4
|
+
include MysqlInspector::TablePart
|
5
|
+
|
6
|
+
def to_sql
|
7
|
+
parts = []
|
8
|
+
parts << "CONSTRAINT"
|
9
|
+
parts << quote(name)
|
10
|
+
parts << "FOREIGN KEY"
|
11
|
+
parts << paren(column_names.map { |c| quote(c) })
|
12
|
+
parts << "REFERENCES"
|
13
|
+
parts << quote(foreign_table)
|
14
|
+
parts << paren(foreign_column_names.map { |c| quote(c) })
|
15
|
+
parts << "ON DELETE #{on_delete}"
|
16
|
+
parts << "ON UPDATE #{on_update}"
|
17
|
+
parts * " "
|
18
|
+
end
|
19
|
+
|
20
|
+
def =~(matcher)
|
21
|
+
name =~ matcher ||
|
22
|
+
column_names.any? { |c| c =~ matcher } ||
|
23
|
+
foreign_table =~ matcher ||
|
24
|
+
foreign_column_names.any? { |c| c =~ matcher }
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module MysqlInspector
|
2
|
+
class Diff
|
3
|
+
|
4
|
+
def initialize(current_dump, target_dump)
|
5
|
+
@current_dump = current_dump
|
6
|
+
@target_dump = target_dump
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :added_tables
|
10
|
+
attr_reader :missing_tables
|
11
|
+
attr_reader :equal_tables
|
12
|
+
attr_reader :different_tables
|
13
|
+
|
14
|
+
def execute
|
15
|
+
@added_tables = []
|
16
|
+
@missing_tables = []
|
17
|
+
@equal_tables = []
|
18
|
+
@different_tables = []
|
19
|
+
|
20
|
+
target_tables = Hash[*@target_dump.tables.map { |t| [t.table_name, t] }.flatten]
|
21
|
+
current_tables = Hash[*@current_dump.tables.map { |t| [t.table_name, t] }.flatten]
|
22
|
+
|
23
|
+
(target_tables.keys + current_tables.keys).uniq.each { |n|
|
24
|
+
target = target_tables[n]
|
25
|
+
current = current_tables[n]
|
26
|
+
if target && current
|
27
|
+
if target == current
|
28
|
+
@equal_tables << target
|
29
|
+
else
|
30
|
+
@different_tables << TableDiff.new(target, current)
|
31
|
+
end
|
32
|
+
else
|
33
|
+
@added_tables << target if target_tables.has_key?(n)
|
34
|
+
@missing_tables << current if current_tables.has_key?(n)
|
35
|
+
end
|
36
|
+
}
|
37
|
+
end
|
38
|
+
|
39
|
+
protected
|
40
|
+
|
41
|
+
class TableDiff
|
42
|
+
|
43
|
+
def initialize(target_table, current_table)
|
44
|
+
@target_table = target_table
|
45
|
+
@current_table = current_table
|
46
|
+
end
|
47
|
+
|
48
|
+
def table_name
|
49
|
+
@target_table.table_name
|
50
|
+
end
|
51
|
+
|
52
|
+
def added_columns
|
53
|
+
@target_table.columns - @current_table.columns
|
54
|
+
end
|
55
|
+
|
56
|
+
def missing_columns
|
57
|
+
@current_table.columns - @target_table.columns
|
58
|
+
end
|
59
|
+
|
60
|
+
def added_indices
|
61
|
+
@target_table.indices - @current_table.indices
|
62
|
+
end
|
63
|
+
|
64
|
+
def missing_indices
|
65
|
+
@current_table.indices - @target_table.indices
|
66
|
+
end
|
67
|
+
|
68
|
+
def added_constraints
|
69
|
+
@target_table.constraints - @current_table.constraints
|
70
|
+
end
|
71
|
+
|
72
|
+
def missing_constraints
|
73
|
+
@current_table.constraints - @target_table.constraints
|
74
|
+
end
|
75
|
+
|
76
|
+
def <=>(other)
|
77
|
+
table_name <=> other.table_name
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
module MysqlInspector
|
2
|
+
class Dump
|
3
|
+
|
4
|
+
def initialize(dir, *extras)
|
5
|
+
@dir = dir
|
6
|
+
@extras = extras
|
7
|
+
end
|
8
|
+
|
9
|
+
# Public: Get the dump directory.
|
10
|
+
#
|
11
|
+
# Returns a String.
|
12
|
+
attr_reader :dir
|
13
|
+
|
14
|
+
# Public: Delete this dump from the filesystem.
|
15
|
+
#
|
16
|
+
# Returns nothing.
|
17
|
+
def clean!
|
18
|
+
FileUtils.rm_rf(dir)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Public: Determine if a dump currently exists at the dump directory.
|
22
|
+
#
|
23
|
+
# Returns a boolean.
|
24
|
+
def exists?
|
25
|
+
Dir[File.join(dir, "*")].any?
|
26
|
+
end
|
27
|
+
|
28
|
+
# Public: Get the tables written by the dump.
|
29
|
+
#
|
30
|
+
# Returns an Array of MysqlInspector::Table.
|
31
|
+
def tables
|
32
|
+
Dir[File.join(dir, "*.table")].map { |file| Table.new(File.read(file)) }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Public: Write to the dump directory. Any existing dump will be deleted.
|
36
|
+
#
|
37
|
+
# access - Instance of Access.
|
38
|
+
#
|
39
|
+
# Returns nothing.
|
40
|
+
def write!(access)
|
41
|
+
clean! if exists?
|
42
|
+
FileUtils.mkdir_p(dir)
|
43
|
+
begin
|
44
|
+
access.tables.each { |table|
|
45
|
+
File.open(File.join(dir, "#{table.table_name}.table"), "w") { |f|
|
46
|
+
f.print table.to_simple_schema
|
47
|
+
}
|
48
|
+
}
|
49
|
+
@extras.each { |extra| extra.write!(access) }
|
50
|
+
rescue
|
51
|
+
FileUtils.rm_rf(dir) # note this does not remove all the dirs that may have been created.
|
52
|
+
raise
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Public: Load this dump into a database. All existing tables will
|
57
|
+
# be deleted from the database and replaced by those from this dump.
|
58
|
+
#
|
59
|
+
# access - Instance of Access.
|
60
|
+
#
|
61
|
+
# Returns nothing.
|
62
|
+
def load!(access)
|
63
|
+
schema = tables.map { |t| t.to_sql }.join(";")
|
64
|
+
access.drop_all_tables
|
65
|
+
access.load(schema)
|
66
|
+
@extras.each { |extra| extra.load!(access) }
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module MysqlInspector
|
2
|
+
class Grep
|
3
|
+
|
4
|
+
def initialize(dump, matchers)
|
5
|
+
@dump = dump
|
6
|
+
@matchers = matchers
|
7
|
+
@columns = []
|
8
|
+
@indices = []
|
9
|
+
@constraints = []
|
10
|
+
end
|
11
|
+
|
12
|
+
attr_reader :columns
|
13
|
+
attr_reader :indices
|
14
|
+
attr_reader :constraints
|
15
|
+
|
16
|
+
def execute
|
17
|
+
@columns = []
|
18
|
+
@indices = []
|
19
|
+
@constraints = []
|
20
|
+
@dump.tables.each { |table|
|
21
|
+
@columns.concat find(table.columns)
|
22
|
+
@indices.concat find(table.indices)
|
23
|
+
@constraints.concat find(table.constraints)
|
24
|
+
}
|
25
|
+
@columns.sort!
|
26
|
+
@indices.sort!
|
27
|
+
@constraints.sort!
|
28
|
+
end
|
29
|
+
|
30
|
+
def any_matches?
|
31
|
+
(columns + indices + constraints).any?
|
32
|
+
end
|
33
|
+
|
34
|
+
def tables
|
35
|
+
(columns + indices + constraints).map { |x| x.table }.uniq.sort
|
36
|
+
end
|
37
|
+
|
38
|
+
def each_table
|
39
|
+
tables.each { |t| yield t, in_table(t) }
|
40
|
+
end
|
41
|
+
|
42
|
+
class Subset < Struct.new(:columns, :indices, :constraints)
|
43
|
+
def any_matches?
|
44
|
+
(columns + indices + constraints).any?
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def in_table(table)
|
49
|
+
Subset.new(
|
50
|
+
@columns.find_all { |c| c.table == table },
|
51
|
+
@indices.find_all { |i| i.table == table },
|
52
|
+
@constraints.find_all { |c| c.table == table }
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
protected
|
57
|
+
|
58
|
+
def find(parts)
|
59
|
+
parts.find_all { |col|
|
60
|
+
@matchers.all? { |m| col =~ m }
|
61
|
+
}
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module MysqlInspector
|
2
|
+
class Index < Struct.new(:name, :column_names, :unique)
|
3
|
+
|
4
|
+
include MysqlInspector::TablePart
|
5
|
+
|
6
|
+
def to_sql
|
7
|
+
parts = []
|
8
|
+
if name == "PRIMARY KEY" && unique
|
9
|
+
parts << "PRIMARY KEY"
|
10
|
+
parts << paren(column_names.map { |c| quote(c) })
|
11
|
+
else
|
12
|
+
parts << "UNIQUE" if unique
|
13
|
+
parts << "KEY"
|
14
|
+
parts << quote(name)
|
15
|
+
parts << paren(column_names.map { |c| quote(c) })
|
16
|
+
end
|
17
|
+
parts * " "
|
18
|
+
end
|
19
|
+
|
20
|
+
def =~(matcher)
|
21
|
+
name =~ matcher || column_names.any? { |c| c =~ matcher }
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module MysqlInspector
|
2
|
+
class Migrations
|
3
|
+
|
4
|
+
def initialize(dir)
|
5
|
+
@dir = dir
|
6
|
+
end
|
7
|
+
|
8
|
+
def write!(access)
|
9
|
+
migrations = access.read_migrations(migrations_table_name)
|
10
|
+
File.open(File.join(@dir, "#{migrations_table_name}.tsv"), "w") { |f|
|
11
|
+
f.puts migrations.sort.join("\n")
|
12
|
+
}
|
13
|
+
end
|
14
|
+
|
15
|
+
def load!(access)
|
16
|
+
access.write_migrations(migrations_table_name, migrations_column, migrations)
|
17
|
+
end
|
18
|
+
|
19
|
+
def migrations_table_name
|
20
|
+
"schema_migrations"
|
21
|
+
end
|
22
|
+
|
23
|
+
def migrations_column
|
24
|
+
"version"
|
25
|
+
end
|
26
|
+
|
27
|
+
def migrations
|
28
|
+
file = File.join(@dir, "#{migrations_table_name}.tsv")
|
29
|
+
if File.exist?(file)
|
30
|
+
File.readlines(file).map { |line| line.chomp }
|
31
|
+
else
|
32
|
+
[]
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require 'mysql_inspector'
|
2
|
+
|
3
|
+
# Ensure that ActiveRecord has initialized.
|
4
|
+
require "active_record/railtie"
|
5
|
+
|
6
|
+
module MysqlInspector
|
7
|
+
class Railtie < Rails::Railtie
|
8
|
+
|
9
|
+
# Store your schema in the MysqlInspector schema format.
|
10
|
+
config.active_record.schema_format = :mysql_inspector
|
11
|
+
|
12
|
+
rake_tasks do
|
13
|
+
load "mysql_inspector/railties/databases.rake"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# This file patches ActiveRecord's databases.rake so that when you dump and
|
2
|
+
# load databases it goes through mysql-inspector. Patching the existing rake
|
3
|
+
# tasks is very brittle do the spaghetti nature of the existing code, so if a
|
4
|
+
# specific task doesn't work please file an issue.
|
5
|
+
|
6
|
+
mysql_inspector_db_namespace = namespace :db do
|
7
|
+
|
8
|
+
def mysql_inspector_config(database=nil)
|
9
|
+
config = MysqlInspector::Config.new
|
10
|
+
config.rails!
|
11
|
+
config.database_name = database if database
|
12
|
+
config
|
13
|
+
end
|
14
|
+
|
15
|
+
|
16
|
+
#
|
17
|
+
# Rewrite public the db:setup task.
|
18
|
+
#
|
19
|
+
|
20
|
+
# Totally reset the setup task.
|
21
|
+
task(:setup).clear_prerequisites.clear_actions
|
22
|
+
|
23
|
+
# Redefine the setup task to use mysql_inspector.
|
24
|
+
task :setup => ['db:create', 'mysql_inspector:load']
|
25
|
+
|
26
|
+
|
27
|
+
#
|
28
|
+
# Rewrite internal the db:_dump task.
|
29
|
+
#
|
30
|
+
|
31
|
+
# Totally reset the _dump task.
|
32
|
+
task(:_dump).clear_prerequisites.clear_actions
|
33
|
+
|
34
|
+
# Redefine the dump task to always call mysql_inspector:dump.
|
35
|
+
task :_dump => "db:mysql_inspector:dump" do
|
36
|
+
# Allow this task to be called as many times as required. An example is the
|
37
|
+
# migrate:redo task, which calls other two internally that depend on this one.
|
38
|
+
mysql_inspector_db_namespace['_dump'].reenable
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
#
|
43
|
+
# Fix that abort_if_pending_migrations does not properly connect to a database.
|
44
|
+
#
|
45
|
+
|
46
|
+
# Reset the prerequisites for the migration check.
|
47
|
+
task(:abort_if_pending_migrations).clear_prerequisites
|
48
|
+
|
49
|
+
# Fix that we need to connect to the dev database.
|
50
|
+
task :abort_if_pending_migrations => :mysql_inspector_connect_to_dev
|
51
|
+
task :mysql_inspector_connect_to_dev do
|
52
|
+
ActiveRecord::Base.establish_connection(:development)
|
53
|
+
end
|
54
|
+
|
55
|
+
|
56
|
+
namespace :test do
|
57
|
+
|
58
|
+
#
|
59
|
+
# Rewrite the public db:prepare task.
|
60
|
+
#
|
61
|
+
|
62
|
+
# Totally reset the db:test:prepare task.
|
63
|
+
task(:prepare).clear_prerequisites.clear_actions
|
64
|
+
|
65
|
+
# Redefine db:test:prepare to load the mysql_inspector structure.
|
66
|
+
task :prepare => ["db:test:purge", "db:abort_if_pending_migrations", "db:mysql_inspector:load_to_test"]
|
67
|
+
end
|
68
|
+
|
69
|
+
|
70
|
+
#
|
71
|
+
# mysql_inspector tasks.
|
72
|
+
#
|
73
|
+
|
74
|
+
namespace :mysql_inspector do
|
75
|
+
|
76
|
+
desc "Write the current development database to db/current"
|
77
|
+
task :dump => :environment do
|
78
|
+
mysql_inspector_config("development").write_dump("current")
|
79
|
+
end
|
80
|
+
|
81
|
+
desc "Load the development database from db/current"
|
82
|
+
task :load => [:environment, :load_config] do
|
83
|
+
mysql_inspector_config("development").load_dump("current")
|
84
|
+
end
|
85
|
+
|
86
|
+
# Internal: Load the test database from db/current.
|
87
|
+
task :load_to_test do
|
88
|
+
mysql_inspector_config("test").load_dump("current")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
end
|