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