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,147 @@
1
+ module MysqlInspector
2
+ class Table
3
+
4
+ BACKTICK_WORD = /`([^`]+)`/
5
+ BACKTICK_CSV = /\(([^\)]+)\)/
6
+ REFERENCE_OPTION = /RESTRICT|CASCADE|SET NULL|NO ACTION/
7
+
8
+ def initialize(schema)
9
+ @schema = schema
10
+ @lines = schema.split("\n")
11
+ @lines.delete_if { |line| line =~ /(\/\*|--)/ or line.strip.empty? }
12
+ end
13
+
14
+ # Public: Get then name of the table.
15
+ #
16
+ # Returns a String.
17
+ def table_name
18
+ @table_name ||= begin
19
+ line = @lines.find { |line| line =~ /CREATE TABLE #{BACKTICK_WORD}/}
20
+ $1 if line
21
+ end
22
+ end
23
+
24
+ # Public: Get all of the columns defined in the table.
25
+ #
26
+ # Returns an Array of MysqlInspector::Column.
27
+ def columns
28
+ @columns ||= @lines.map { |line|
29
+ if line.strip =~ /^#{BACKTICK_WORD} ([\w\(\)\d]+)/
30
+ name = $1
31
+ sql_type = $2
32
+ nullable = !!(line !~ /NOT NULL/)
33
+ default = line[/DEFAULT ('?[^']+'?)/, 1]
34
+ default = nil if default =~ /NULL/
35
+ auto_increment = !!(line =~ /AUTO_INCREMENT/)
36
+ table_part line, MysqlInspector::Column.new(name, sql_type, nullable, default, auto_increment)
37
+ end
38
+ }.compact.sort
39
+ end
40
+
41
+ # Public: Get all of the indices defined in the table
42
+ #
43
+ # Returns an Array of MysqlInspector::Index.
44
+ def indices
45
+ @indices ||= @lines.map { |line|
46
+ if line.strip =~ /^(UNIQUE )?KEY #{BACKTICK_WORD} #{BACKTICK_CSV}/
47
+ unique = !!$1
48
+ name = $2
49
+ column_names = backtick_names_in_csv($3)
50
+ table_part line, MysqlInspector::Index.new(name, column_names, unique)
51
+ elsif line.strip =~ /^PRIMARY KEY #{BACKTICK_CSV}/
52
+ unique = true
53
+ name = "PRIMARY KEY"
54
+ column_names = backtick_names_in_csv($1)
55
+ table_part line, MysqlInspector::Index.new(name, column_names, unique)
56
+ end
57
+ }.compact.sort
58
+ end
59
+
60
+ # Public: Get all of the constraints defined in the table
61
+ #
62
+ # Returns an Array of MysqlInspector::Constraint.
63
+ def constraints
64
+ @constraints ||= @lines.map { |line|
65
+ if line.strip =~ /^CONSTRAINT #{BACKTICK_WORD} FOREIGN KEY #{BACKTICK_CSV} REFERENCES #{BACKTICK_WORD} #{BACKTICK_CSV} ON DELETE (#{REFERENCE_OPTION}) ON UPDATE (#{REFERENCE_OPTION})$/
66
+ name = $1
67
+ column_names = backtick_names_in_csv($2)
68
+ foreign_name = $3
69
+ foreign_column_names = backtick_names_in_csv($4)
70
+ on_delete = $5
71
+ on_update = $6
72
+ table_part line, MysqlInspector::Constraint.new(name, column_names, foreign_name, foreign_column_names, on_update, on_delete)
73
+ end
74
+ }.compact.sort
75
+ end
76
+
77
+ def options
78
+ @options ||= begin
79
+ if line = @lines.find { |line| line =~ /ENGINE=/}
80
+ # AUTO_INCREMENT is not useful.
81
+ line.sub!(/AUTO_INCREMENT=\d+/, '')
82
+ # Compact multiple spaces.
83
+ line.gsub!(/\s+/, ' ')
84
+ # Remove paren at the beginning.
85
+ line.sub!(/^\)\s*/, '')
86
+ # Remove semicolon at the end.
87
+ line.chomp!(';')
88
+ line
89
+ end
90
+ end
91
+ end
92
+
93
+ def eql?(other)
94
+ table_name == other.table_name &&
95
+ columns == other.columns &&
96
+ indices == other.indices &&
97
+ constraints == other.constraints &&
98
+ options = other.options
99
+ end
100
+
101
+ alias == eql?
102
+
103
+ def <=>(other)
104
+ table_name <=> other.table_name
105
+ end
106
+
107
+ def to_simple_schema
108
+ lines = []
109
+
110
+ lines << "CREATE TABLE `#{table_name}`"
111
+ lines << nil
112
+ simple_schema_items(lines, columns)
113
+ simple_schema_items(lines, indices)
114
+ simple_schema_items(lines, constraints)
115
+ lines << options
116
+
117
+ lines.join("\n")
118
+ end
119
+
120
+ def to_sql
121
+ lines = []
122
+
123
+ lines << "CREATE TABLE `#{table_name}` ("
124
+ lines << (columns + indices + constraints).map { |x| " #{x.to_sql}" }.join(",\n")
125
+ lines << ") #{options}"
126
+
127
+ lines.join("\n")
128
+ end
129
+
130
+ protected
131
+
132
+ def simple_schema_items(lines, items)
133
+ lines.concat items.map { |item| item.to_sql }
134
+ lines << nil if items.any?
135
+ end
136
+
137
+ def table_part(line, part)
138
+ part.table = self
139
+ part
140
+ end
141
+
142
+ def backtick_names_in_csv(string)
143
+ string.split(',').map { |x| x[BACKTICK_WORD, 1] }
144
+ end
145
+
146
+ end
147
+ end
@@ -0,0 +1,21 @@
1
+ module MysqlInspector
2
+ module TablePart
3
+
4
+ attr_accessor :table
5
+
6
+ def <=>(other)
7
+ name <=> other.name
8
+ end
9
+
10
+ protected
11
+
12
+ def quote(word)
13
+ "`#{word}`"
14
+ end
15
+
16
+ def paren(words)
17
+ "(#{words * ","})"
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,3 @@
1
+ module MysqlInspector
2
+ VERSION = "0.1.0"
3
+ end
@@ -1,44 +1,25 @@
1
- # Generated by jeweler
2
- # DO NOT EDIT THIS FILE DIRECTLY
3
- # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
1
  # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "mysql_inspector/version"
5
4
 
6
5
  Gem::Specification.new do |s|
7
- s.name = %q{mysql-inspector}
8
- s.version = "0.0.6"
6
+ s.name = "mysql-inspector"
7
+ s.version = MysqlInspector::VERSION
8
+ s.authors = ["Ryan Carver"]
9
+ s.email = ["ryan@ryancarver.com"]
10
+ s.homepage = ""
11
+ s.summary = %q{Store and understand your MySQL schema}
12
+ s.description = %q{Store and understand your MySQL schema}
9
13
 
10
- s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
- s.authors = ["Ryan Carver"]
12
- s.date = %q{2010-03-23}
13
- s.default_executable = %q{mysql-inspector}
14
- s.email = %q{ryan@fivesevensix.com}
15
- s.executables = ["mysql-inspector"]
16
- s.extra_rdoc_files = [
17
- "README"
18
- ]
19
- s.files = [
20
- ".gitignore",
21
- "README",
22
- "Rakefile",
23
- "VERSION",
24
- "bin/mysql-inspector",
25
- "lib/mysql-inspector.rb",
26
- "mysql-inspector.gemspec"
27
- ]
28
- s.homepage = %q{http://github.com/rcarver/mysql-inspector}
29
- s.rdoc_options = ["--charset=UTF-8"]
14
+ s.rubyforge_project = "mysql-inspector"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
30
19
  s.require_paths = ["lib"]
31
- s.rubygems_version = %q{1.3.6}
32
- s.summary = %q{Tools for identifying changes to a MySQL schema}
33
20
 
34
- if s.respond_to? :specification_version then
35
- current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
36
- s.specification_version = 3
21
+ s.add_development_dependency "rake"
22
+ s.add_development_dependency "minitest"
37
23
 
38
- if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
39
- else
40
- end
41
- else
42
- end
24
+ # s.add_runtime_dependency "rest-client"
43
25
  end
44
-
@@ -0,0 +1,7 @@
1
+ class CreateUsers < ActiveRecord::Migration
2
+ def change
3
+ create_table(:users) do |t|
4
+ t.string :name
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,9 @@
1
+ class CreateThings < ActiveRecord::Migration
2
+ def change
3
+ create_table(:things) do |t|
4
+ t.references :user
5
+ t.string :name
6
+ end
7
+ end
8
+ end
9
+
@@ -0,0 +1,125 @@
1
+ require 'minitest/autorun'
2
+ require 'minitest/mock'
3
+ require 'open3'
4
+ require 'ostruct'
5
+ require 'stringio'
6
+
7
+ require 'mysql_inspector'
8
+
9
+ require 'helpers/mysql_utils'
10
+ require 'helpers/mysql_schemas'
11
+ require 'helpers/string_unindented'
12
+
13
+ class MysqlInspectorSpec < MiniTest::Spec
14
+ include MysqlSchemas
15
+
16
+ def it msg
17
+ raise "A block must not be passed to the example-level +it+" if block_given?
18
+ @__current_msg = "it #{msg}"
19
+ end
20
+
21
+ def message msg = nil, ending = ".", &default
22
+ super(msg || @__current_msg, ending, &default)
23
+ end
24
+
25
+ register_spec_type(self) { |desc| true }
26
+
27
+ # Create a temporary directory. This directory will exist for the life of
28
+ # the spec.
29
+ #
30
+ # id - Identifier of the tmpdir (default: the default identifier).
31
+ #
32
+ # Returns a String.
33
+ def tmpdir(id=:default)
34
+ @tmpdirs[id] ||= Dir.mktmpdir
35
+ end
36
+
37
+ # Get the name of the test database.
38
+ #
39
+ # Returns a String.
40
+ def database_name
41
+ "mysql_inspector_test"
42
+ end
43
+
44
+ # Create a test mysql database. The database will exist for the life
45
+ # of the spec.
46
+ #
47
+ # schema - String schema to create (default: no schema).
48
+ #
49
+ # Returns nothing.
50
+ def create_mysql_database(schema="")
51
+ @mysql_database = true
52
+ MysqlUtils.create_mysql_database(database_name, schema)
53
+ end
54
+
55
+ # Drop the test mysql database.
56
+ #
57
+ # Returns nothing.
58
+ def drop_mysql_database
59
+ MysqlUtils.drop_mysql_database(database_name)
60
+ end
61
+
62
+ # Get access to the mysql database.
63
+ #
64
+ # Returns a MysqlInspector:Access.
65
+ def access
66
+ (@access ||= {})[database_name] ||= MysqlInspector::Access.new(database_name, "root", nil, "mysql")
67
+ end
68
+
69
+ let(:config) { MysqlInspector::Config.new }
70
+
71
+ before do
72
+ @tmpdirs = {}
73
+ @mysql_database = nil
74
+ end
75
+
76
+ after do
77
+ @tmpdirs.values.each { |dir| FileUtils.rm_rf dir }
78
+ drop_mysql_database if @mysql_database
79
+ end
80
+ end
81
+
82
+ class MysqlInspectorCliSpec < MysqlInspectorSpec
83
+
84
+ register_spec_type(self) { |desc| desc =~ /mysql-inspector/ }
85
+
86
+ before do
87
+ config.dir = tmpdir
88
+ end
89
+
90
+ def parse_command(klass, argv)
91
+ command = klass.new(config, StringIO.new, StringIO.new)
92
+ command.parse!(argv)
93
+ command
94
+ end
95
+
96
+ def run_command(klass, argv)
97
+ command = klass.new(config, StringIO.new, StringIO.new)
98
+ command.parse!(argv)
99
+ command.run!
100
+ command
101
+ end
102
+
103
+ def mysql_inspector(args)
104
+ cli = MysqlInspector::CLI.new(config, StringIO.new, StringIO.new)
105
+ argv = args.split(/\s+/).map { |x| x.gsub(/'/, '') }
106
+ cli.run!(argv)
107
+ cli
108
+ end
109
+
110
+ def inspect_database(args)
111
+ mysql_inspector args
112
+ end
113
+
114
+ def stdout
115
+ subject.stdout.string.chomp
116
+ end
117
+
118
+ def stderr
119
+ subject.stderr.string.chomp
120
+ end
121
+
122
+ def status
123
+ subject.status
124
+ end
125
+ end
@@ -0,0 +1,37 @@
1
+ require 'active_record'
2
+ require 'mysql2'
3
+ require 'helper'
4
+
5
+ class MysqlInspectorActiveRecordSpec < MysqlInspectorSpec
6
+
7
+ register_spec_type(self) { |desc| desc =~ /activerecord/ }
8
+
9
+ before do
10
+ unless ActiveRecord::Base.connected?
11
+ ActiveRecord::Base.establish_connection(
12
+ :adapter => :mysql2,
13
+ :database => database_name,
14
+ :username => "root",
15
+ :password => nil
16
+ )
17
+ end
18
+ create_mysql_database
19
+ config.migrations = true
20
+ end
21
+
22
+ # Execute all of the fixture migrations.
23
+ #
24
+ # Returns nothing.
25
+ def run_active_record_migrations!
26
+ ActiveRecord::Migration.verbose = false
27
+ ActiveRecord::Migrator.migrate(["test/fixtures/migrate"])
28
+ end
29
+
30
+ # Get access to the mysql database.
31
+ #
32
+ # Returns a MysqlInspector:AR::Access.
33
+ def access
34
+ MysqlInspector::AR::Access.new(ActiveRecord::Base.connection)
35
+ end
36
+
37
+ end
@@ -0,0 +1,82 @@
1
+ require 'helpers/string_unindented'
2
+
3
+ # Sample table schemas used for testing.
4
+ module MysqlSchemas
5
+
6
+ # A sample starting database.
7
+ def schema_a
8
+ [ideas_schema, colors_schema, things_schema_1].join(";\n")
9
+ end
10
+
11
+ # A sample changed database.
12
+ def schema_b
13
+ [users_schema, ideas_schema, things_schema_2].join(";\n")
14
+ end
15
+
16
+ def colors_schema
17
+ <<-STR.unindented
18
+ CREATE TABLE `colors` (
19
+ `name` varchar(255) NOT NULL,
20
+ UNIQUE KEY `colors_primary` (`name`)
21
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
22
+ STR
23
+ end
24
+
25
+ def ideas_schema
26
+ <<-STR.unindented
27
+ CREATE TABLE `ideas` (
28
+ `id` int(11) NOT NULL AUTO_INCREMENT,
29
+ `name` varchar(255) NOT NULL,
30
+ `description` text NOT NULL,
31
+ PRIMARY KEY (`id`),
32
+ KEY `name` (`name`)
33
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
34
+ STR
35
+ end
36
+
37
+ def users_schema
38
+ <<-STR.unindented
39
+ CREATE TABLE `users` (
40
+ `id` int(11) NOT NULL AUTO_INCREMENT,
41
+ `first_name` varchar(255) NOT NULL,
42
+ `last_name` varchar(255) NOT NULL,
43
+ UNIQUE KEY `users_primary` (`id`),
44
+ KEY `name` (`first_name`,`last_name`)
45
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
46
+ STR
47
+ end
48
+
49
+ def things_schema_1
50
+ <<-STR.unindented
51
+ CREATE TABLE `things` (
52
+ `id` int(11) NOT NULL AUTO_INCREMENT,
53
+ `name` varchar(255) NOT NULL DEFAULT 'toy',
54
+ `weight` int(11) DEFAULT NULL,
55
+ `color` varchar(255) NOT NULL,
56
+ UNIQUE KEY `things_primary` (`id`),
57
+ KEY `color` (`color`),
58
+ CONSTRAINT `belongs_to_color` FOREIGN KEY (`color`) REFERENCES `colors` (`name`) ON DELETE NO ACTION ON UPDATE CASCADE
59
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
60
+ STR
61
+ end
62
+
63
+ def things_schema_2
64
+ <<-STR.unindented
65
+ CREATE TABLE `things` (
66
+ `id` int(11) NOT NULL AUTO_INCREMENT,
67
+ `name` varchar(255) NOT NULL DEFAULT 'toy',
68
+ `weight` int(11) DEFAULT NULL,
69
+ `first_name` varchar(255) NOT NULL,
70
+ `last_name` varchar(255) NOT NULL,
71
+ UNIQUE KEY `things_primary` (`id`),
72
+ KEY `name` (`first_name`,`last_name`),
73
+ CONSTRAINT `belongs_to_user` FOREIGN KEY (`first_name`, `last_name`) REFERENCES `users` (`first_name`, `last_name`) ON DELETE NO ACTION ON UPDATE CASCADE
74
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8
75
+ STR
76
+ end
77
+
78
+ def things_schema
79
+ things_schema_2
80
+ end
81
+ end
82
+