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