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.
Files changed (48) hide show
  1. data/.gitignore +5 -3
  2. data/CHANGELOG.md +8 -0
  3. data/Gemfile +6 -0
  4. data/LICENSE +20 -0
  5. data/README.md +82 -0
  6. data/Rakefile +36 -14
  7. data/bin/mysql-inspector +9 -85
  8. data/lib/mysql-inspector.rb +1 -226
  9. data/lib/mysql_inspector.rb +30 -0
  10. data/lib/mysql_inspector/access.rb +64 -0
  11. data/lib/mysql_inspector/ar/access.rb +55 -0
  12. data/lib/mysql_inspector/cli.rb +291 -0
  13. data/lib/mysql_inspector/column.rb +21 -0
  14. data/lib/mysql_inspector/config.rb +82 -0
  15. data/lib/mysql_inspector/constraint.rb +28 -0
  16. data/lib/mysql_inspector/diff.rb +82 -0
  17. data/lib/mysql_inspector/dump.rb +70 -0
  18. data/lib/mysql_inspector/grep.rb +65 -0
  19. data/lib/mysql_inspector/index.rb +25 -0
  20. data/lib/mysql_inspector/migrations.rb +37 -0
  21. data/lib/mysql_inspector/railtie.rb +17 -0
  22. data/lib/mysql_inspector/railties/databases.rake +92 -0
  23. data/lib/mysql_inspector/table.rb +147 -0
  24. data/lib/mysql_inspector/table_part.rb +21 -0
  25. data/lib/mysql_inspector/version.rb +3 -0
  26. data/mysql-inspector.gemspec +17 -36
  27. data/test/fixtures/migrate/111_create_users.rb +7 -0
  28. data/test/fixtures/migrate/222_create_things.rb +9 -0
  29. data/test/helper.rb +125 -0
  30. data/test/helper_ar.rb +37 -0
  31. data/test/helpers/mysql_schemas.rb +82 -0
  32. data/test/helpers/mysql_utils.rb +35 -0
  33. data/test/helpers/string_unindented.rb +13 -0
  34. data/test/mysql_inspector/cli_basics_test.rb +77 -0
  35. data/test/mysql_inspector/cli_diff_test.rb +60 -0
  36. data/test/mysql_inspector/cli_grep_test.rb +74 -0
  37. data/test/mysql_inspector/cli_load_test.rb +43 -0
  38. data/test/mysql_inspector/cli_write_test.rb +58 -0
  39. data/test/mysql_inspector/config_test.rb +14 -0
  40. data/test/mysql_inspector/diff_test.rb +82 -0
  41. data/test/mysql_inspector/dump_test.rb +81 -0
  42. data/test/mysql_inspector/grep_test.rb +61 -0
  43. data/test/mysql_inspector/table_test.rb +123 -0
  44. data/test/mysql_inspector_ar/ar_dump_test.rb +29 -0
  45. data/test/mysql_inspector_ar/ar_migrations_test.rb +47 -0
  46. metadata +123 -49
  47. data/README +0 -48
  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