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.
- data/.gitignore +5 -3
- data/CHANGELOG.md +8 -0
- data/Gemfile +6 -0
- data/LICENSE +20 -0
- data/README.md +82 -0
- data/Rakefile +36 -14
- data/bin/mysql-inspector +9 -85
- data/lib/mysql-inspector.rb +1 -226
- data/lib/mysql_inspector.rb +30 -0
- data/lib/mysql_inspector/access.rb +64 -0
- data/lib/mysql_inspector/ar/access.rb +55 -0
- data/lib/mysql_inspector/cli.rb +291 -0
- data/lib/mysql_inspector/column.rb +21 -0
- data/lib/mysql_inspector/config.rb +82 -0
- data/lib/mysql_inspector/constraint.rb +28 -0
- data/lib/mysql_inspector/diff.rb +82 -0
- data/lib/mysql_inspector/dump.rb +70 -0
- data/lib/mysql_inspector/grep.rb +65 -0
- data/lib/mysql_inspector/index.rb +25 -0
- data/lib/mysql_inspector/migrations.rb +37 -0
- data/lib/mysql_inspector/railtie.rb +17 -0
- data/lib/mysql_inspector/railties/databases.rake +92 -0
- data/lib/mysql_inspector/table.rb +147 -0
- data/lib/mysql_inspector/table_part.rb +21 -0
- data/lib/mysql_inspector/version.rb +3 -0
- data/mysql-inspector.gemspec +17 -36
- data/test/fixtures/migrate/111_create_users.rb +7 -0
- data/test/fixtures/migrate/222_create_things.rb +9 -0
- data/test/helper.rb +125 -0
- data/test/helper_ar.rb +37 -0
- data/test/helpers/mysql_schemas.rb +82 -0
- data/test/helpers/mysql_utils.rb +35 -0
- data/test/helpers/string_unindented.rb +13 -0
- data/test/mysql_inspector/cli_basics_test.rb +77 -0
- data/test/mysql_inspector/cli_diff_test.rb +60 -0
- data/test/mysql_inspector/cli_grep_test.rb +74 -0
- data/test/mysql_inspector/cli_load_test.rb +43 -0
- data/test/mysql_inspector/cli_write_test.rb +58 -0
- data/test/mysql_inspector/config_test.rb +14 -0
- data/test/mysql_inspector/diff_test.rb +82 -0
- data/test/mysql_inspector/dump_test.rb +81 -0
- data/test/mysql_inspector/grep_test.rb +61 -0
- data/test/mysql_inspector/table_test.rb +123 -0
- data/test/mysql_inspector_ar/ar_dump_test.rb +29 -0
- data/test/mysql_inspector_ar/ar_migrations_test.rb +47 -0
- metadata +123 -49
- data/README +0 -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
|