convergence 0.0.1

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 (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +3 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +32 -0
  5. data/.travis.yml +7 -0
  6. data/Gemfile +3 -0
  7. data/Gemfile.lock +88 -0
  8. data/Guardfile +5 -0
  9. data/LICENSE.txt +22 -0
  10. data/README.md +40 -0
  11. data/Rakefile +34 -0
  12. data/bin/convergence +23 -0
  13. data/convergence-0.0.1.gem +0 -0
  14. data/convergence.gemspec +33 -0
  15. data/database.yml.example +5 -0
  16. data/lib/convergence.rb +24 -0
  17. data/lib/convergence/column.rb +36 -0
  18. data/lib/convergence/command.rb +56 -0
  19. data/lib/convergence/command/apply.rb +51 -0
  20. data/lib/convergence/command/diff.rb +26 -0
  21. data/lib/convergence/command/dryrun.rb +39 -0
  22. data/lib/convergence/command/export.rb +15 -0
  23. data/lib/convergence/config.rb +17 -0
  24. data/lib/convergence/database_connector.rb +25 -0
  25. data/lib/convergence/database_connector/mysql_connector.rb +27 -0
  26. data/lib/convergence/default_parameter.rb +32 -0
  27. data/lib/convergence/default_parameter/mysql_default_parameter.rb +159 -0
  28. data/lib/convergence/diff.rb +148 -0
  29. data/lib/convergence/dsl.rb +20 -0
  30. data/lib/convergence/dumper.rb +68 -0
  31. data/lib/convergence/dumper/mysql_schema_dumper.rb +149 -0
  32. data/lib/convergence/foreign_key.rb +11 -0
  33. data/lib/convergence/index.rb +9 -0
  34. data/lib/convergence/logger.rb +12 -0
  35. data/lib/convergence/module.rb +1 -0
  36. data/lib/convergence/pretty_diff.rb +55 -0
  37. data/lib/convergence/sql_generator.rb +2 -0
  38. data/lib/convergence/sql_generator/mysql_generator.rb +208 -0
  39. data/lib/convergence/table.rb +37 -0
  40. data/lib/convergence/version.rb +3 -0
  41. data/spec/config/spec_database.yml +6 -0
  42. data/spec/convergence/diff_spec.rb +242 -0
  43. data/spec/convergence/dsl_spec.rb +78 -0
  44. data/spec/convergence/dumper/mysql_schema_dumper_spec.rb +87 -0
  45. data/spec/convergence/dumper_spec.rb +40 -0
  46. data/spec/convergence/table_spec.rb +106 -0
  47. data/spec/fixtures/add_columns_to_paper.schema +25 -0
  48. data/spec/fixtures/add_table.schema +28 -0
  49. data/spec/fixtures/change_comment_columns_to_paper.schema +24 -0
  50. data/spec/fixtures/change_table_comment_to_paper.schema +24 -0
  51. data/spec/fixtures/drop_table.schema +15 -0
  52. data/spec/fixtures/remove_columns_to_paper.schema +23 -0
  53. data/spec/fixtures/test_db.sql +27 -0
  54. data/spec/integrations/command_dryrun.rb +73 -0
  55. data/spec/spec_helper.rb +18 -0
  56. metadata +268 -0
@@ -0,0 +1,51 @@
1
+ require 'benchmark'
2
+
3
+ class Convergence::Command::Apply < Convergence::Command
4
+ def validate!
5
+ fail ArgumentError.new('--config required') if @config.nil?
6
+ fail ArgumentError.new('--input required') unless @opts[:input]
7
+ end
8
+
9
+ def execute
10
+ validate!
11
+ input_tables = Convergence::DSL.parse(File.open(@opts[:input]).read)
12
+ current_tables = dumper.dump
13
+ execute_sql(input_tables, current_tables)
14
+ end
15
+
16
+ def execute_sql(input_tables, current_tables)
17
+ sql = generate_sql(input_tables, current_tables)
18
+ unless sql.strip.empty?
19
+ sql = <<-SQL
20
+ SET FOREIGN_KEY_CHECKS=0;
21
+ #{sql}
22
+ SET FOREIGN_KEY_CHECKS=1;
23
+ SQL
24
+ end
25
+ sql.split(';').each do |q2|
26
+ q = q2.strip
27
+ unless q.empty?
28
+ begin
29
+ q = q + ';'
30
+ time = Benchmark.realtime { connector.client.query(q) }
31
+ logger.output q
32
+ logger.output " --> #{time}s"
33
+ rescue => e
34
+ logger.output 'Invalid Query Exception >>>'
35
+ logger.output q
36
+ logger.output '<<<'
37
+ throw e
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ def generate_sql(input_tables, current_tables)
44
+ current_tables_with_full_option =
45
+ Convergence::DefaultParameter.append_database_default_parameter(current_tables, database_adapter)
46
+ input_tables_with_full_option =
47
+ Convergence::DefaultParameter.append_database_default_parameter(input_tables, database_adapter)
48
+ delta = Convergence::Diff.new.diff(current_tables_with_full_option, input_tables_with_full_option)
49
+ sql_generator.generate(input_tables_with_full_option, delta)
50
+ end
51
+ end
@@ -0,0 +1,26 @@
1
+ class Convergence::Command::Diff < Convergence::Command
2
+ def validate!
3
+ unless @opts[:diff].size == 2
4
+ fail ArgumentError.new('diff required two arguments')
5
+ end
6
+ end
7
+
8
+ def execute
9
+ validate!
10
+ from = Convergence::DefaultParameter.remove_database_default_parameter(from_tables, database_adapter)
11
+ to = Convergence::DefaultParameter.remove_database_default_parameter(to_tables, database_adapter)
12
+ msg = Convergence::PrettyDiff.new(from, to).output
13
+ logger.output(msg)
14
+ msg
15
+ end
16
+
17
+ private
18
+
19
+ def from_tables
20
+ Convergence::DSL.parse(File.open(@opts[:diff][0]).read)
21
+ end
22
+
23
+ def to_tables
24
+ Convergence::DSL.parse(File.open(@opts[:diff][1]).read)
25
+ end
26
+ end
@@ -0,0 +1,39 @@
1
+ class Convergence::Command::Dryrun < Convergence::Command
2
+ def validate!
3
+ fail ArgumentError.new('--config required') if @config.nil?
4
+ fail ArgumentError.new('--input required') unless @opts[:input]
5
+ end
6
+
7
+ def execute
8
+ validate!
9
+ input_tables = Convergence::DSL.parse(File.open(@opts[:input]).read)
10
+ current_tables = dumper.dump
11
+ # -- maybe it's redundant output
12
+ # output_diff(input_tables, current_tables)
13
+ output_sql(input_tables, current_tables)
14
+ end
15
+
16
+ def output_diff(input_tables, current_tables)
17
+ input_tables_without_default_parameter =
18
+ Convergence::DefaultParameter.remove_database_default_parameter(input_tables, database_adapter)
19
+ current_tables_without_default_parameter =
20
+ Convergence::DefaultParameter.remove_database_default_parameter(current_tables, database_adapter)
21
+
22
+ msg = Convergence::PrettyDiff
23
+ .new(current_tables_without_default_parameter, input_tables_without_default_parameter)
24
+ .output
25
+ logger.output(msg)
26
+ msg
27
+ end
28
+
29
+ def output_sql(input_tables, current_tables)
30
+ msg = Convergence::Command::Apply
31
+ .new(@opts, config: @config)
32
+ .generate_sql(input_tables, current_tables)
33
+ .split("\n")
34
+ .map { |v| '# ' + v }
35
+ .join("\n")
36
+ logger.output(msg)
37
+ msg
38
+ end
39
+ end
@@ -0,0 +1,15 @@
1
+ class Convergence::Command::Export < Convergence::Command
2
+ def validate!
3
+ if @config.nil?
4
+ fail ArgumentError.new('--config required')
5
+ end
6
+ end
7
+
8
+ def execute
9
+ validate!
10
+ tables = Convergence::DefaultParameter.remove_database_default_parameter(dumper.dump, database_adapter)
11
+ msg = Convergence::Dumper.new.dump_dsl(tables)
12
+ logger.output(msg)
13
+ msg
14
+ end
15
+ end
@@ -0,0 +1,17 @@
1
+ require 'erb'
2
+ require 'yaml'
3
+
4
+ class Convergence::Config
5
+ attr_accessor :adapter, :database, :host, :port, :username, :password
6
+
7
+ def initialize(attributes)
8
+ attributes.each do |k, v|
9
+ instance_variable_set("@#{k}", v) unless v.nil?
10
+ end
11
+ end
12
+
13
+ def self.load(yaml_path)
14
+ setting = YAML.load(ERB.new(File.read(yaml_path)).result)
15
+ new(setting)
16
+ end
17
+ end
@@ -0,0 +1,25 @@
1
+ class Convergence::DatabaseConnector
2
+ attr_reader :connector
3
+
4
+ def initialize(config)
5
+ @connector =
6
+ case config.adapter
7
+ when 'mysql'
8
+ Convergence::DatabaseConnector::MysqlConnector.new(config)
9
+ else
10
+ fail NotImplementedError.new("#{config.adapter} not supported yet")
11
+ end
12
+ end
13
+
14
+ def client
15
+ @connector.client
16
+ end
17
+
18
+ def schema_client
19
+ @connector.schema_client
20
+ end
21
+
22
+ def config
23
+ @connector.config
24
+ end
25
+ end
@@ -0,0 +1,27 @@
1
+ class Convergence::DatabaseConnector::MysqlConnector
2
+ attr_reader :config
3
+
4
+ def initialize(config)
5
+ @config = config
6
+ end
7
+
8
+ def client(database_name = @config.database)
9
+ @mysql ||= Mysql2::Client.new(
10
+ host: @config.host,
11
+ port: @config.port,
12
+ username: @config.username,
13
+ password: @config.password,
14
+ database: database_name
15
+ )
16
+ end
17
+
18
+ def schema_client
19
+ @schema_mysql ||= Mysql2::Client.new(
20
+ host: @config.host,
21
+ port: @config.port,
22
+ username: @config.username,
23
+ password: @config.password,
24
+ database: 'information_schema'
25
+ )
26
+ end
27
+ end
@@ -0,0 +1,32 @@
1
+ class Convergence::DefaultParameter
2
+ def initialize(adapter)
3
+ case adapter.downcase
4
+ when 'mysql'
5
+ @parameter_klass = Convergence::DefaultParameter::MysqlDefaultParameter.new
6
+ else
7
+ fail NotImplementedError.new("unknown adapter #{config.adapter}.")
8
+ end
9
+ end
10
+
11
+ def remove_default_parameter(table)
12
+ @parameter_klass.remove_default_parameter(table)
13
+ end
14
+
15
+ def append_default_parameter(table)
16
+ @parameter_klass.append_default_parameter(table)
17
+ end
18
+
19
+ def self.remove_database_default_parameter(tables, adapter)
20
+ values = tables.values.map do |table|
21
+ { table.table_name => Convergence::DefaultParameter.new(adapter).remove_default_parameter(table) }
22
+ end
23
+ values.reduce { |a, e| a.merge(e) }
24
+ end
25
+
26
+ def self.append_database_default_parameter(tables, adapter)
27
+ values = tables.values.map do |table|
28
+ { table.table_name => Convergence::DefaultParameter.new(adapter).append_default_parameter(table) }
29
+ end
30
+ values.reduce { |a, e| a.merge(e) }
31
+ end
32
+ end
@@ -0,0 +1,159 @@
1
+ class Convergence::DefaultParameter::MysqlDefaultParameter
2
+ DEFAULT_TABLE_PARAMETERS = {
3
+ engine: 'InnoDB',
4
+ row_format: 'Compact',
5
+ default_charset: 'utf8'
6
+ }
7
+ DEFAULT_COLLATE_NAME = {
8
+ 'big5' => 'big5_chinese_ci',
9
+ 'dec8' => 'dec8_swedish_ci',
10
+ 'cp850' => 'cp850_general_ci',
11
+ 'hp8' => ' hp8_english_ci',
12
+ 'koi8r' => ' koi8r_general_ci',
13
+ 'latin1' => 'latin1_swedish_ci',
14
+ 'latin2' => 'latin2_general_ci',
15
+ 'swe7' => 'swe7_swedish_ci',
16
+ 'ascii' => 'ascii_general_ci',
17
+ 'ujis' => 'ujis_japanese_ci',
18
+ 'sjis' => 'sjis_japanese_ci',
19
+ 'hebrew' => 'hebrew_general_ci',
20
+ 'tis620' => 'tis620_thai_ci',
21
+ 'euckr' => 'euckr_korean_ci',
22
+ 'koi8u' => 'koi8u_general_ci',
23
+ 'gb2312' => 'gb2312_chinese_ci',
24
+ 'greek' => ' greek_general_ci',
25
+ 'cp1250' => 'cp1250_general_ci',
26
+ 'gbk' => 'gbk_chinese_ci',
27
+ 'latin5' => 'latin5_turkish_ci',
28
+ 'armscii8' => 'armscii8_general_ci',
29
+ 'utf8' => 'utf8_general_ci',
30
+ 'ucs2' => 'ucs2_general_ci',
31
+ 'cp866' => 'cp866_general_ci',
32
+ 'keybcs2' => 'keybcs2_general_ci',
33
+ 'macce' => 'macce_general_ci',
34
+ 'macroman' => 'macroman_general_ci',
35
+ 'cp852' => 'cp852_general_ci',
36
+ 'latin7' => 'latin7_general_ci',
37
+ 'utf8mb4' => 'utf8mb4_general_ci',
38
+ 'cp1251' => 'cp1251_general_ci',
39
+ 'utf16' => 'utf16_general_ci',
40
+ 'utf16le' => 'utf16le_general_ci',
41
+ 'cp1256' => 'cp1256_general_ci',
42
+ 'cp1257' => 'cp1257_general_ci',
43
+ 'utf32' => 'utf32_general_ci',
44
+ 'binary' => 'binary',
45
+ 'geostd8' => 'eostd8_general_ci',
46
+ 'cp932' => 'cp932_japanese_ci',
47
+ 'eucjpms' => 'ucjpms_japanese_ci'
48
+ }
49
+ DEFAULT_COLUMN_PARAMETERS = {
50
+ null: false
51
+ }
52
+ TEXT_TYPE = [:varchar, :char, :tiny_text, :text, :mediumtext, :longtext]
53
+ DEFAULT_COLUMN_TYPE_PARAMETERS = {
54
+ tinyint: {
55
+ limit: 3
56
+ },
57
+ smallint: {
58
+ limit: 5
59
+ },
60
+ mediumint: {
61
+ limit: 8
62
+ },
63
+ int: {
64
+ limit: 11
65
+ },
66
+ bigint: {
67
+ limit: 19
68
+ },
69
+ varchar: {
70
+ limit: 255
71
+ }
72
+ }
73
+ DEFAULT_INDEX_PARAMETERS = { type: 'BTREE' }
74
+
75
+ def initialize
76
+ end
77
+
78
+ def remove_default_parameter(table)
79
+ remove_column_default_parameter(table)
80
+ remove_table_default_parameter(table)
81
+ remove_index_default_parameter(table)
82
+ table
83
+ end
84
+
85
+ def append_default_parameter(table)
86
+ append_column_default_parameter(table)
87
+ append_table_default_parameter(table)
88
+ append_index_default_parameter(table)
89
+ table
90
+ end
91
+
92
+ private
93
+
94
+ def remove_column_default_parameter(table)
95
+ table.columns.each do |_column_name, column|
96
+ type = column.type
97
+ parameters = DEFAULT_COLUMN_PARAMETERS.merge(DEFAULT_COLUMN_TYPE_PARAMETERS[type] || {})
98
+ if TEXT_TYPE.include?(type)
99
+ character_set = table.table_options[:default_charset] || DEFAULT_TABLE_PARAMETERS[:default_charset]
100
+ parameters = parameters.merge(
101
+ character_set: character_set,
102
+ collate: table.table_options[:collate] || DEFAULT_COLLATE_NAME[character_set.downcase])
103
+ end
104
+ parameters.each do |k, v|
105
+ if !column.options[k].nil? && column.options[k].to_s.downcase == v.to_s.downcase
106
+ column.options.delete(k)
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ def remove_table_default_parameter(table)
113
+ table.table_options.each do |k, v|
114
+ if !DEFAULT_TABLE_PARAMETERS[k].nil? && DEFAULT_TABLE_PARAMETERS[k].downcase == v.to_s.downcase
115
+ table.table_options.delete(k)
116
+ end
117
+ end
118
+ end
119
+
120
+ def remove_index_default_parameter(table)
121
+ table.indexes.each do |_, va|
122
+ va.options.each do |k, v|
123
+ if !DEFAULT_INDEX_PARAMETERS[k].nil? && DEFAULT_INDEX_PARAMETERS[k].downcase == v.to_s.downcase
124
+ va.options.delete(k)
125
+ end
126
+ end
127
+ end
128
+ end
129
+
130
+ def append_column_default_parameter(table)
131
+ table.columns.each do |_column_name, column|
132
+ type = column.type
133
+ parameters = DEFAULT_COLUMN_PARAMETERS
134
+ .merge(DEFAULT_COLUMN_TYPE_PARAMETERS[type] || {})
135
+ .merge(column.options)
136
+ if TEXT_TYPE.include?(type)
137
+ character_set = table.table_options[:default_charset] || DEFAULT_TABLE_PARAMETERS[:default_charset]
138
+ parameters = parameters.merge(
139
+ character_set: character_set,
140
+ collate: table.table_options[:collate] || DEFAULT_COLLATE_NAME[character_set.downcase])
141
+ end
142
+ column.options = parameters
143
+ end
144
+ end
145
+
146
+ def append_table_default_parameter(table)
147
+ table.table_options = DEFAULT_TABLE_PARAMETERS.merge(table.table_options)
148
+ if table.table_options[:collate].nil?
149
+ table.table_options.merge!(collate: DEFAULT_COLLATE_NAME[table.table_options[:default_charset].downcase])
150
+ end
151
+ end
152
+
153
+ def append_index_default_parameter(table)
154
+ table.indexes.each do |_column_name, column|
155
+ parameters = DEFAULT_INDEX_PARAMETERS.merge(column.options)
156
+ column.options = parameters
157
+ end
158
+ end
159
+ end
@@ -0,0 +1,148 @@
1
+ require 'diff/lcs'
2
+
3
+ class Convergence::Diff
4
+ def diff(from_database, to_database)
5
+ delta = {}
6
+ from_database = {} if from_database.nil?
7
+ delta[:add_table] = scan_add_table(from_database, to_database)
8
+ delta[:remove_table] = scan_remove_table(from_database, to_database)
9
+ delta[:change_table] = scan_change_table(from_database, to_database)
10
+ delta
11
+ end
12
+
13
+ def diff_table(from_table, to_table)
14
+ from = from_table.dup
15
+ to = to_table.dup
16
+ delta = {}
17
+ delta[:add_column] = scan_add_column(from, to)
18
+ delta[:remove_column] = scan_remove_column(from, to)
19
+ delta[:change_column] = scan_change_column(from, to)
20
+ scan_change_order_column(from, to, delta)
21
+ delta[:remove_index] = scan_change_index(to, from)
22
+ delta[:add_index] = scan_change_index(from, to)
23
+ delta[:remove_foreign_key] = scan_change_foreign_key(to, from)
24
+ delta[:add_foreign_key] = scan_change_foreign_key(from, to)
25
+ delta[:change_table_option] = scan_change_table_option(from, to)
26
+ delta
27
+ end
28
+
29
+ private
30
+
31
+ def scan_add_table(from, to)
32
+ to.reject { |table_name, _| from.map { |k, _| k }.include?(table_name) }
33
+ end
34
+
35
+ def scan_remove_table(from, to)
36
+ from.reject { |table_name, _| to.map { |k, _| k }.include?(table_name) }
37
+ end
38
+
39
+ def scan_change_table(from, to)
40
+ delta = {}
41
+ target_tables = from.map { |name, _| name } & to.map { |name, _| name }
42
+ target_tables.each do |target_table|
43
+ from_table = from.find { |name, _| name == target_table }[1]
44
+ to_table = to.find { |name, _| name == target_table }[1]
45
+ diff = diff_table(from_table, to_table)
46
+ unless diff.values.all?(&:empty?)
47
+ delta[target_table] = diff
48
+ end
49
+ end
50
+ delta
51
+ end
52
+
53
+ def scan_add_column(from, to)
54
+ to.columns.reject { |column_name, _| from.columns.keys.include?(column_name) }
55
+ end
56
+
57
+ def scan_remove_column(from, to)
58
+ from.columns.reject { |column_name, _| to.columns.keys.include?(column_name) }
59
+ end
60
+
61
+ def scan_change_column(from, to)
62
+ change_columns = from
63
+ .columns
64
+ .map do |column_name, from_column|
65
+ to_column = to.columns[column_name]
66
+ if to_column
67
+ to_column_option_with_type = (from_column.options.map { |k, _v| { k => nil } }.reduce { |a, e| a.merge(e) } || {})
68
+ .merge(to_column.options)
69
+ .merge(type: to_column.type)
70
+ .map { |k, v| [k, v.to_s.downcase] }
71
+ .to_a
72
+ from_column_option_with_type = from_column
73
+ .options
74
+ .merge(type: from_column.type)
75
+ .map { |k, v| [k, v.to_s.downcase] }
76
+ .to_a
77
+ { column_name => Hash[(to_column_option_with_type - from_column_option_with_type)] }
78
+ end
79
+ end
80
+ change_columns
81
+ .compact
82
+ .reduce { |a, e| a.merge(e) }
83
+ .reject { |_k, v| v.empty? }
84
+ end
85
+
86
+ def scan_change_order_column(from, to, delta)
87
+ from_columns = from.columns.keys
88
+ to_columns = to.columns.keys
89
+ order_changed_columns = Diff::LCS.diff(from_columns, to_columns)
90
+ .flatten
91
+ .select(&:adding?)
92
+ .map(&:element)
93
+ order_changed_columns.each do |column|
94
+ before_column_index = to_columns.index { |v| v == column } - 1
95
+ before_column = if before_column_index < 0
96
+ nil
97
+ else
98
+ to_columns[before_column_index]
99
+ end
100
+ if delta[:add_column][column]
101
+ delta[:add_column][column].options.merge!(after: before_column)
102
+ else
103
+ delta[:change_column][column] = {} if delta[:change_column][column].nil?
104
+ delta[:change_column][column].merge!(after: before_column)
105
+ end
106
+ end
107
+ end
108
+
109
+ def scan_change_index(from, to)
110
+ delta = {}
111
+ to.indexes.each do |name, index|
112
+ candidate_index = from.indexes.find { |from_name, _| from_name == name }
113
+ if candidate_index.nil? || candidate_index[1].options != index.options
114
+ delta[name] = index
115
+ end
116
+ end
117
+ delta
118
+ end
119
+
120
+ def scan_change_foreign_key(from, to)
121
+ delta = {}
122
+ to.foreign_keys.each do |name, fk|
123
+ candidate_foreign_keys = from.foreign_keys.find { |from_name, _| from_name == name }
124
+ target_fk = candidate_foreign_keys[1] rescue nil
125
+ if candidate_foreign_keys.nil?
126
+ delta[name] = fk
127
+ elsif !target_fk.nil?
128
+ if target_fk.from_columns != fk.from_columns ||
129
+ target_fk.key_name != fk.key_name ||
130
+ target_fk.options != fk.options ||
131
+ target_fk.to_columns != fk.to_columns ||
132
+ target_fk.to_table != fk.to_table
133
+ delta[name] = fk
134
+ end
135
+ end
136
+ end
137
+ delta
138
+ end
139
+
140
+ def scan_change_table_option(from, to)
141
+ change_options = (from.table_options.map { |k, _v| { k => nil } }.reduce { |a, e| a.merge(e) } || {})
142
+ .merge(to.table_options)
143
+ .reject do |k, v|
144
+ !from.table_options[k].nil? && from.table_options[k].to_s.downcase == v.to_s.downcase
145
+ end
146
+ Hash[change_options]
147
+ end
148
+ end