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