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,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
|