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,30 @@
1
+ require "fileutils"
2
+ require 'open3'
3
+ require "time"
4
+ require 'tempfile'
5
+
6
+ require "mysql_inspector/version"
7
+
8
+ require "mysql_inspector/table_part"
9
+ require "mysql_inspector/column"
10
+ require "mysql_inspector/constraint"
11
+ require "mysql_inspector/index"
12
+
13
+ require "mysql_inspector/cli"
14
+ require "mysql_inspector/config"
15
+ require "mysql_inspector/diff"
16
+ require "mysql_inspector/grep"
17
+ require "mysql_inspector/migrations"
18
+ require "mysql_inspector/table"
19
+
20
+ require "mysql_inspector/access"
21
+ require "mysql_inspector/dump"
22
+
23
+ require "mysql_inspector/ar/access"
24
+
25
+ if defined?(Rails)
26
+ require 'mysql_inspector/railtie'
27
+ end
28
+
29
+ module MysqlInspector
30
+ end
@@ -0,0 +1,64 @@
1
+ module MysqlInspector
2
+ class Access
3
+
4
+ Error = Class.new(StandardError)
5
+
6
+ def initialize(database_name, mysql_user, mysql_password, mysql_path)
7
+ @database_name = database_name
8
+ @mysql_user = mysql_user
9
+ @mysql_password = mysql_password
10
+ @mysql_path = mysql_path
11
+ end
12
+
13
+ def table_names
14
+ rows_from pipe_to_mysql("SHOW TABLES")
15
+ end
16
+
17
+ def tables
18
+ table_names.map { |table|
19
+ rows = rows_from pipe_to_mysql("SHOW CREATE TABLE #{table}")
20
+ schema = rows[0].split("\t").last.gsub(/\\n/, "\n")
21
+ MysqlInspector::Table.new(schema)
22
+ }
23
+ end
24
+
25
+ def drop_all_tables
26
+ return if table_names.empty?
27
+
28
+ pipe_to_mysql without_foreign_keys("DROP TABLE #{table_names.join(',')}")
29
+ end
30
+
31
+ def load(schema)
32
+ pipe_to_mysql without_foreign_keys(schema)
33
+ end
34
+
35
+ protected
36
+
37
+ def without_foreign_keys(query)
38
+ ["SET foreign_key_checks = 0", query, "SET foreign_key_checks = 1"].join(";\n")
39
+ end
40
+
41
+ def pipe_to_mysql(query)
42
+ mysql_command = [@mysql_path, "-u#{@mysql_user}", @mysql_password ? "-p#{@mysql_password}" : nil].compact * " "
43
+ out, err, status = nil
44
+ Tempfile.open('schema') do |file|
45
+ file.print(query)
46
+ file.flush
47
+ out, err, status = Open3.capture3("cat #{file.path} | #{mysql_command} #{@database_name}")
48
+ end
49
+ unless status.exitstatus == 0
50
+ case err
51
+ when /\s1049\s/
52
+ raise Error, "The database #{@database_name} does not exist"
53
+ else
54
+ raise Error, err
55
+ end
56
+ end
57
+ out
58
+ end
59
+
60
+ def rows_from(output)
61
+ (output.split("\n")[1..-1] || []).map { |row| row.chomp }
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,55 @@
1
+ module MysqlInspector
2
+ module AR
3
+ class Access
4
+
5
+ def initialize(connection)
6
+ @connection = connection
7
+ end
8
+
9
+ def table_names
10
+ tables.map { |t| t.table_name }
11
+ end
12
+
13
+ def tables
14
+ dump = @connection.structure_dump
15
+ tables = dump.split(";").map { |schema|
16
+ table = MysqlInspector::Table.new(schema)
17
+ table if table.table_name
18
+ }.compact
19
+ end
20
+
21
+ def drop_all_tables
22
+ @connection.disable_referential_integrity do
23
+ names = table_names
24
+ @connection.execute("DROP TABLE #{names.join(',')}") if names.any?
25
+ end
26
+ end
27
+
28
+ def load(schema)
29
+ @connection.disable_referential_integrity do
30
+ schema.split(";").each { |table|
31
+ @connection.execute(table)
32
+ }
33
+ end
34
+ end
35
+
36
+ def read_migrations(table_name)
37
+ if table_names.include?(table_name)
38
+ @connection.select_values("SELECT * FROM #{table_name}")
39
+ else
40
+ []
41
+ end
42
+ end
43
+
44
+ def write_migrations(table_name, col, values)
45
+ if table_names.include?(table_name)
46
+ values = values.map { |value| "('#{value}')" }
47
+ @connection.execute("INSERT INTO #{table_name} (#{col}) VALUES #{values * ','}")
48
+ end
49
+ end
50
+ end
51
+
52
+ end
53
+ end
54
+
55
+
@@ -0,0 +1,291 @@
1
+ require 'optparse'
2
+
3
+ module MysqlInspector
4
+ class CLI
5
+
6
+ NAME = "mysql-inspector"
7
+
8
+ CURRENT = "current"
9
+ TARGET = "target"
10
+
11
+ module Helper
12
+
13
+ def exit(msg)
14
+ @stdout.puts msg
15
+ throw :quit, 0
16
+ end
17
+
18
+ def abort(msg)
19
+ @stderr.puts msg
20
+ throw :quit, 1
21
+ end
22
+
23
+ def usage(msg)
24
+ abort "Usage: #{NAME} #{msg}"
25
+ end
26
+
27
+ def puts(*args)
28
+ @stdout.puts(*args)
29
+ end
30
+ end
31
+
32
+ module Formatting
33
+ # Print table item details.
34
+ #
35
+ # Examples
36
+ #
37
+ # LABEL item1
38
+ # item2
39
+ #
40
+ def format_items(label, items, &formatter)
41
+ pad = " " * 4
42
+ formatter ||= proc { |item | item.to_sql }
43
+ items.each.with_index { |item, i|
44
+ if i == 0
45
+ puts [label, pad, formatter.call(item)] * ""
46
+ else
47
+ puts [" " * label.size, pad, formatter.call(item)] * ""
48
+ end
49
+ }
50
+ end
51
+ end
52
+
53
+ include Helper
54
+
55
+ def initialize(config, stdout, stderr)
56
+ @config = config
57
+ @stdout = stdout
58
+ @stderr = stderr
59
+ @status = 0
60
+ end
61
+
62
+ attr_reader :stdout
63
+ attr_reader :stderr
64
+ attr_reader :status
65
+
66
+ def option_parser
67
+ @option_parser ||= OptionParser.new do |opts|
68
+ opts.banner = "Usage: #{NAME} [options] command [command args]"
69
+
70
+ opts.separator ""
71
+ opts.separator "Options"
72
+ opts.separator ""
73
+
74
+ opts.on("--out DIR", "Where to store schemas. Defaults to '.'") do |dir|
75
+ @config.dir = dir
76
+ end
77
+
78
+ opts.on("--rails", "Configure for a Rails project") do
79
+ @config.rails!
80
+ end
81
+
82
+ opts.on("-h", "--help", "What you're looking at") do
83
+ exit opts.to_s
84
+ end
85
+
86
+ opts.on("-v", "--version", "Show the version of mysql-inspector") do
87
+ exit MysqlInspector::VERSION
88
+ end
89
+
90
+ opts.separator ""
91
+ opts.separator "Commands"
92
+ opts.separator ""
93
+
94
+ opts.separator " write DATABASE [VERSION]"
95
+ opts.separator " load DATABASE [VERSION]"
96
+ opts.separator " diff"
97
+ opts.separator " diff TO"
98
+ opts.separator " diff FROM TO"
99
+ opts.separator " grep PATTERN [PATTERN]"
100
+ opts.separator ""
101
+ end
102
+ end
103
+
104
+ def get_command(argv)
105
+ option_parser.parse!(argv)
106
+
107
+ command_name = argv.shift or abort option_parser.to_s
108
+ command_class = command_name.capitalize + "Command"
109
+
110
+ begin
111
+ klass = self.class.const_get(command_class)
112
+ command = klass.new(@config, @stdout, @stderr)
113
+ command.parse!(argv)
114
+ command
115
+ rescue NameError
116
+ abort "Unknown command #{command_name.inspect}"
117
+ end
118
+ end
119
+
120
+ def run!(argv)
121
+ @status = catch(:quit) {
122
+ command = get_command(argv)
123
+ command.run!
124
+ }
125
+ end
126
+
127
+ class Command
128
+ include Helper
129
+
130
+ def initialize(config, stdout, stderr)
131
+ @config = config
132
+ @stdout = stdout
133
+ @stderr = stderr
134
+ @status = nil
135
+ end
136
+
137
+ attr_reader :config
138
+ attr_reader :stdout
139
+ attr_reader :stderr
140
+ attr_reader :status
141
+
142
+ def ivar(name)
143
+ instance_variable_get("@#{name}")
144
+ end
145
+
146
+ def get_dump(version)
147
+ dump = @config.create_dump(version)
148
+ dump.exists? or abort "Dump #{version.inspect} does not exist"
149
+ dump
150
+ end
151
+
152
+ def parse!(argv)
153
+ @status = catch(:quit) {
154
+ parse(argv)
155
+ }
156
+ end
157
+
158
+ def run!
159
+ @status = catch(:quit) {
160
+ begin
161
+ run
162
+ throw :quit, 0
163
+ rescue MysqlInspector::Access::Error => e
164
+ abort e.message
165
+ end
166
+ }
167
+ end
168
+ end
169
+
170
+ class WriteCommand < Command
171
+
172
+ def parse(argv)
173
+ @database = argv.shift or usage "write DATABASE [VERSION]"
174
+ @version = argv.shift || CURRENT
175
+ end
176
+
177
+ def run
178
+ config.database_name = @database
179
+ config.write_dump(@version)
180
+ end
181
+ end
182
+
183
+ class LoadCommand < Command
184
+
185
+ def parse(argv)
186
+ @database = argv.shift or usage "load DATABASE [VERSION]"
187
+ @version = argv.shift || CURRENT
188
+ end
189
+
190
+ def run
191
+ get_dump(@version) # ensure it exists
192
+ config.database_name = @database
193
+ config.load_dump(@version)
194
+ end
195
+ end
196
+
197
+ class GrepCommand < Command
198
+ include Formatting
199
+
200
+ def parse(argv)
201
+ @version = CURRENT
202
+ @matchers = *argv.map { |a| Regexp.new(a) }
203
+ end
204
+
205
+ def run
206
+ dump = get_dump(@version)
207
+
208
+ grep = Grep.new(dump, @matchers)
209
+ grep.execute
210
+
211
+ puts "grep #{@matchers.map { |m| m.inspect } * " AND "}"
212
+
213
+ puts if grep.any_matches?
214
+
215
+ grep.each_table { |table, subset|
216
+ puts table.table_name
217
+ format_items("COL", subset.columns)
218
+ format_items("IDX", subset.indices)
219
+ format_items("CST", subset.constraints)
220
+ puts
221
+ }
222
+ end
223
+ end
224
+
225
+ class DiffCommand < Command
226
+ include Formatting
227
+
228
+ def parse(argv)
229
+ case argv.size
230
+ when 0
231
+ @version1 = CURRENT
232
+ @version2 = TARGET
233
+ when 1
234
+ @version1 = CURRENT
235
+ @version2 = argv.shift
236
+ else
237
+ @version1 = argv.shift
238
+ @version2 = argv.shift
239
+ end
240
+ end
241
+
242
+ def run
243
+ dump1 = get_dump(@version1)
244
+ dump2 = get_dump(@version2)
245
+
246
+ diff = Diff.new(dump1, dump2)
247
+ diff.execute
248
+
249
+ puts "diff #{@version1} #{@version2}"
250
+
251
+ tables = diff.added_tables + diff.missing_tables + diff.different_tables
252
+
253
+ if tables.any?
254
+ puts
255
+ tables.sort.each do |t|
256
+ prefix = prefix_for_table(t, diff)
257
+ puts "#{prefix} #{t.table_name}"
258
+ if t.is_a?(Diff::TableDiff)
259
+ format_diff_items(" COL", t.added_columns, t.missing_columns)
260
+ format_diff_items(" IDX", t.added_indices, t.missing_indices)
261
+ format_diff_items(" CST", t.added_constraints, t.missing_constraints)
262
+ end
263
+ end
264
+ puts
265
+ end
266
+ end
267
+
268
+ protected
269
+
270
+ def prefix_for_table(table, diff)
271
+ case
272
+ when diff.added_tables.include?(table) then "+"
273
+ when diff.missing_tables.include?(table) then "-"
274
+ else "="
275
+ end
276
+ end
277
+
278
+ def prefix_for_item(item, added, removed)
279
+ added.include?(item) ? "+" : "-"
280
+ end
281
+
282
+ def format_diff_items(label, added, removed)
283
+ format_items(label, (added + removed).sort) { |item|
284
+ prefix = prefix_for_item(item, added, removed)
285
+ "#{prefix} #{item.to_sql}"
286
+ }
287
+ end
288
+ end
289
+
290
+ end
291
+ end
@@ -0,0 +1,21 @@
1
+ module MysqlInspector
2
+ class Column < Struct.new(:name, :sql_type, :nullable, :default, :auto_increment)
3
+
4
+ include MysqlInspector::TablePart
5
+
6
+ def to_sql
7
+ parts = []
8
+ parts << quote(name)
9
+ parts << sql_type
10
+ parts << (nullable ? "NULL" : "NOT NULL")
11
+ parts << "DEFAULT #{default}" if default
12
+ parts << "AUTO_INCREMENT" if auto_increment
13
+ parts * " "
14
+ end
15
+
16
+ def =~(matcher)
17
+ name =~ matcher
18
+ end
19
+
20
+ end
21
+ end