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,20 @@
1
+ class Convergence::DSL
2
+ attr_accessor :tables
3
+
4
+ def initialize
5
+ @tables = {}
6
+ end
7
+
8
+ def create_table(table_name, options = {}, &block)
9
+ table = Convergence::Table.new(table_name, options)
10
+ block.call(table)
11
+ @tables[table_name.to_s] = table
12
+ table
13
+ end
14
+
15
+ def self.parse(code)
16
+ parser = new
17
+ parser.instance_eval(code)
18
+ parser.tables
19
+ end
20
+ end
@@ -0,0 +1,68 @@
1
+ class Convergence::Dumper
2
+ def dump_dsl(tables)
3
+ tables.map do |_, table|
4
+ dump_table_dsl(table)
5
+ end.join("\n\n")
6
+ end
7
+
8
+ def dump_table_dsl(table)
9
+ table_argument = ["\"#{table.table_name}\""]
10
+ table_argument << table.table_options.map { |k, v| key_value_text(k, v) }
11
+ dsl = "create_table #{table_argument.flatten.join(', ')} do |t|\n"
12
+ dsl += " #{table.columns.map { |_, column| dump_column(column) }.join("\n ")}"
13
+ dsl += "\n" if !table.indexes.empty? || !table.foreign_keys.empty?
14
+ dsl += "\n"
15
+ unless table.indexes.empty?
16
+ dsl += " #{table.indexes.map { |_, index| dump_index(index) }.join("\n ")}"
17
+ dsl += "\n"
18
+ end
19
+ unless table.foreign_keys.empty?
20
+ dsl += " #{table.foreign_keys.map { |_, key| dump_foreign_key(key) }.join("\n ")}"
21
+ dsl += "\n"
22
+ end
23
+ dsl += 'end'
24
+ dsl
25
+ end
26
+
27
+ private
28
+
29
+ def dump_column(column)
30
+ argument = [%("#{column.column_name}")]
31
+ argument << column.options.map { |k, v| key_value_text(k, v) }
32
+ "t.#{column.type} #{argument.flatten.join(', ')}"
33
+ end
34
+
35
+ def dump_index(index)
36
+ columns = single_or_multiple_text(index.index_columns)
37
+ argument = [columns]
38
+ argument << index.options.map { |k, v| key_value_text(k, v) }
39
+ "t.index #{argument.flatten.join(', ')}"
40
+ end
41
+
42
+ def dump_foreign_key(foreign_key)
43
+ columns = single_or_multiple_text(foreign_key.from_columns)
44
+ argument = [columns]
45
+ argument << [key_value_text('reference', foreign_key.to_table)]
46
+ argument << ["reference_column: #{single_or_multiple_text(foreign_key.to_columns)}"]
47
+ argument << foreign_key.options.map { |k, v| key_value_text(k, v) }
48
+ "t.foreign_key #{argument.flatten.join(', ')}"
49
+ end
50
+
51
+ def single_or_multiple_text(values)
52
+ values_array = [values].flatten
53
+ if values_array.size == 1
54
+ %("#{values_array.first}")
55
+ else
56
+ %(#{values})
57
+ end
58
+ end
59
+
60
+ def key_value_text(k, v)
61
+ value = if v.to_s == 'true' || v.to_s == 'false' || v =~ /^\d+$/
62
+ v
63
+ else
64
+ %(#{v.inspect})
65
+ end
66
+ "#{k}: #{value}"
67
+ end
68
+ end
@@ -0,0 +1,149 @@
1
+ require 'mysql2'
2
+ class Convergence::Dumper::MysqlSchemaDumper
3
+ def initialize(connector)
4
+ @connector = connector
5
+ @target_database = connector.config.database
6
+ @tables = {}
7
+ end
8
+
9
+ def dump
10
+ table_definitions = select_table_definitions(@target_database)
11
+ column_definitions = select_column_definitions(@target_database).group_by { |r| r['TABLE_NAME'] }
12
+ index_definitions = select_index_definitions(@target_database).group_by { |r| r['TABLE_NAME'] }
13
+ table_definitions.map { |r| r['TABLE_NAME'] }.each do |table_name|
14
+ table = Convergence::Table.new(table_name)
15
+ parse_table_options(table, table_definitions.find { |r| r['TABLE_NAME'] == table_name })
16
+ parse_columns(table, column_definitions[table_name])
17
+ parse_indexes(table, index_definitions[table_name])
18
+ @tables[table_name] = table
19
+ end
20
+ @tables
21
+ end
22
+
23
+ private
24
+
25
+ def mysql
26
+ @connector.schema_client
27
+ end
28
+
29
+ def select_table_definitions(database_name)
30
+ mysql.query("
31
+ SELECT
32
+ *
33
+ FROM
34
+ TABLES
35
+ INNER JOIN
36
+ COLLATION_CHARACTER_SET_APPLICABILITY CCSA
37
+ ON
38
+ TABLES.TABLE_COLLATION = CCSA.COLLATION_NAME
39
+ WHERE
40
+ TABLE_SCHEMA = '#{mysql.escape(database_name)}'
41
+ ORDER BY
42
+ TABLE_NAME
43
+ ")
44
+ end
45
+
46
+ def select_column_definitions(database_name)
47
+ mysql.query("
48
+ SELECT * FROM COLUMNS
49
+ WHERE TABLE_SCHEMA = '#{mysql.escape(database_name)}'
50
+ ORDER BY TABLE_NAME, ORDINAL_POSITION
51
+ ")
52
+ end
53
+
54
+ def select_index_definitions(database_name)
55
+ mysql.query("
56
+ SELECT
57
+ DISTINCT S.TABLE_NAME, S.COLUMN_NAME, S.NON_UNIQUE, S.INDEX_NAME, S.SEQ_IN_INDEX, S.INDEX_TYPE,
58
+ IF(TC.CONSTRAINT_TYPE IS NULL, 'INDEX', TC.CONSTRAINT_TYPE) CONSTRAINT_TYPE,
59
+ KCU.REFERENCED_TABLE_NAME, KCU.REFERENCED_COLUMN_NAME
60
+ FROM
61
+ STATISTICS S
62
+ LEFT OUTER JOIN
63
+ TABLE_CONSTRAINTS TC
64
+ ON
65
+ TC.TABLE_SCHEMA = S.TABLE_SCHEMA
66
+ AND TC.TABLE_NAME = S.TABLE_NAME
67
+ AND TC.CONSTRAINT_NAME = S.INDEX_NAME
68
+ LEFT OUTER JOIN
69
+ KEY_COLUMN_USAGE KCU
70
+ ON
71
+ KCU.CONSTRAINT_SCHEMA = S.TABLE_SCHEMA
72
+ AND KCU.TABLE_NAME = S.TABLE_NAME
73
+ AND KCU.CONSTRAINT_NAME = TC.CONSTRAINT_NAME
74
+ WHERE
75
+ S.TABLE_SCHEMA = '#{mysql.escape(database_name)}'
76
+ ORDER BY
77
+ S.TABLE_NAME, S.INDEX_NAME, S.SEQ_IN_INDEX
78
+ ")
79
+ end
80
+
81
+ def parse_table_options(table, table_option)
82
+ option = {}
83
+ option.merge!(engine: table_option['ENGINE'])
84
+ row_format = table_option['CREATE_OPTIONS'].scan(/=(.*)/).flatten[0] || table_option['ROW_FORMAT']
85
+ option.merge!(row_format: row_format)
86
+ option.merge!(default_charset: table_option['CHARACTER_SET_NAME'])
87
+ option.merge!(collate: table_option['TABLE_COLLATION'])
88
+ option.merge!(comment: table_option['TABLE_COMMENT'])
89
+ table.table_options = option
90
+ end
91
+
92
+ def parse_columns(table, columns)
93
+ columns.each do |column|
94
+ data_type, column_name, options = parse_column(column)
95
+ table.send(data_type, column_name, options)
96
+ end
97
+ end
98
+
99
+ def parse_column(column)
100
+ data_type = column['DATA_TYPE']
101
+ column_name = column['COLUMN_NAME']
102
+ options = { null: column['IS_NULLABLE'] == 'YES' ? true : false }
103
+ options.merge!(default: column['COLUMN_DEFAULT']) unless column['COLUMN_DEFAULT'].nil?
104
+ options.merge!(character_set: column['CHARACTER_SET_NAME']) unless column['CHARACTER_SET_NAME'].nil?
105
+ options.merge!(collate: column['COLLATION_NAME']) unless column['COLLATION_NAME'].nil?
106
+ column_type = column['COLUMN_TYPE']
107
+ if data_type == 'enum' || data_type == 'set'
108
+ # TODO: implement
109
+ elsif data_type == 'decimal'
110
+ precision, scale = column_type.scan(/\d+/)
111
+ options.merge!(precision: precision, scale: scale)
112
+ else
113
+ limit = column_type.scan(/\d+/)[0]
114
+ options.merge!(limit: limit) unless limit.nil?
115
+ end
116
+ options.merge!(extra: column['EXTRA']) unless column['EXTRA'].empty?
117
+ options.merge!(comment: column['COLUMN_COMMENT']) unless column['COLUMN_COMMENT'].empty?
118
+ [data_type, column_name, options]
119
+ end
120
+
121
+ def parse_indexes(table, table_indexes)
122
+ table_indexes.group_by { |r| r['INDEX_NAME'] }.each do |index_name, indexes|
123
+ type = indexes.first['CONSTRAINT_TYPE']
124
+ columns = indexes.map { |v| v['COLUMN_NAME'] }
125
+ case type
126
+ when 'PRIMARY KEY'
127
+ indexes.map { |r| r['COLUMN_NAME'] }.each do |column|
128
+ options = { primary_key: true }.merge(table.columns[column].options)
129
+ table.columns[column].options = options
130
+ end
131
+ when 'INDEX', 'UNIQUE'
132
+ options = { name: index_name, type: indexes.first['INDEX_TYPE'] }
133
+ options.merge!(unique: true) if type == 'UNIQUE'
134
+ table.index(columns, options)
135
+ when 'FOREIGN KEY'
136
+ to_table = indexes.first['REFERENCED_TABLE_NAME']
137
+ to_columns = indexes.map { |v| v['REFERENCED_COLUMN_NAME'] }
138
+ options = {
139
+ reference: to_table,
140
+ reference_column: to_columns,
141
+ name: index_name
142
+ }
143
+ table.foreign_key(columns, options)
144
+ else
145
+ fail NotImplementedError.new('Unknown index type')
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,11 @@
1
+ class Convergence::ForeignKey
2
+ attr_accessor :key_name, :from_columns, :to_table, :to_columns, :options
3
+
4
+ def initialize(key_name, from_columns, to_table, to_columns, options)
5
+ @key_name = key_name
6
+ @from_columns = [from_columns].flatten.map(&:to_s)
7
+ @to_table = to_table
8
+ @to_columns = [to_columns].flatten.map(&:to_s)
9
+ @options = { name: @key_name }.merge(options)
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ class Convergence::Index
2
+ attr_accessor :index_name, :index_columns, :options
3
+
4
+ def initialize(index_name, index_columns, options)
5
+ @index_name = index_name
6
+ @index_columns = [index_columns].flatten.map(&:to_s)
7
+ @options = { name: @index_name }.merge(options)
8
+ end
9
+ end
@@ -0,0 +1,12 @@
1
+ require 'logger'
2
+ class Convergence::Logger < Logger
3
+ def initialize
4
+ super($default_output || $stdout)
5
+ self.formatter = proc { |_, _, _, msg| "#{msg}\n" }
6
+ self.level = Logger::INFO
7
+ end
8
+
9
+ def output(msg)
10
+ info(msg)
11
+ end
12
+ end
@@ -0,0 +1 @@
1
+ module Convergence; end
@@ -0,0 +1,55 @@
1
+ require 'diffy'
2
+
3
+ class Convergence::PrettyDiff
4
+ def initialize(from_tables, to_tables)
5
+ @from_tables = from_tables
6
+ @to_tables = to_tables
7
+ end
8
+
9
+ def output
10
+ diff = Convergence::Diff.new.diff(@from_tables, @to_tables)
11
+ add_tables = diff[:add_table].keys
12
+ remove_tables = diff[:remove_table].keys
13
+ change_tables = diff[:change_table].keys
14
+
15
+ results = ''
16
+ add_tables.each do |table_name|
17
+ results += diff_add_table(table_name)
18
+ results += "\n\n"
19
+ end
20
+ remove_tables.each do |table_name|
21
+ results += diff_remove_table(table_name)
22
+ results += "\n\n"
23
+ end
24
+ change_tables.each do |table_name|
25
+ results += diff_change_table(table_name)
26
+ end
27
+ results
28
+ end
29
+
30
+ private
31
+
32
+ def diff_add_table(table_name)
33
+ Convergence::Dumper
34
+ .new
35
+ .dump_table_dsl(@to_tables[table_name])
36
+ .split("\n")
37
+ .map { |v| "+ #{v}" }
38
+ .join("\n")
39
+ end
40
+
41
+ def diff_remove_table(table_name)
42
+ Convergence::Dumper
43
+ .new
44
+ .dump_table_dsl(@from_tables[table_name])
45
+ .split("\n")
46
+ .map { |v| "- #{v}" }
47
+ .join("\n")
48
+ end
49
+
50
+ def diff_change_table(table_name)
51
+ from = Convergence::Dumper.new.dump_table_dsl(@from_tables[table_name]) + "\n"
52
+ to = Convergence::Dumper.new.dump_table_dsl(@to_tables[table_name]) + "\n"
53
+ Diffy::Diff.new(from, to).to_s
54
+ end
55
+ end
@@ -0,0 +1,2 @@
1
+ class SQLGenerator
2
+ end
@@ -0,0 +1,208 @@
1
+ class SQLGenerator::MysqlGenerator < SQLGenerator
2
+ OPTION_MAPPING = {
3
+ engine: 'ENGINE',
4
+ row_format: 'ROW_FORMAT',
5
+ default_charset: 'DEFAULT CHARACTER SET',
6
+ collate: 'COLLATE',
7
+ comment: 'COMMENT'
8
+ }
9
+ QUOTE_OPTION = [:comment]
10
+
11
+ def generate(to_table, delta)
12
+ sqls = []
13
+ sqls << change_table_sql(to_table, delta)
14
+ sqls << ['']
15
+ sqls << create_table_sqls(delta)
16
+ sqls << drop_table_sqls(delta)
17
+ sqls.join("\n")
18
+ end
19
+
20
+ private
21
+
22
+ # FIXME: multiple pk change not supported yet
23
+ def change_table_sql(to_table, delta)
24
+ change_table = delta[:change_table]
25
+ results = []
26
+ change_table.each do |table_name, table_delta|
27
+ table_delta[:remove_column].each do |_column_name, column|
28
+ results << alter_remove_column_sql(table_name, column)
29
+ end
30
+ table_delta[:add_column].each do |_column_name, column|
31
+ results << alter_add_column_sql(table_name, column)
32
+ end
33
+ table_delta[:change_column].each do |column_name, column|
34
+ results << alter_change_column_sql(table_name, column_name, column, to_table)
35
+ end
36
+ table_delta[:remove_index].each do |index_name, _index|
37
+ results << alter_remove_index_sql(table_name, index_name)
38
+ end
39
+ table_delta[:add_index].each do |_index_name, index|
40
+ results << alter_add_index_sql(table_name, index)
41
+ end
42
+ table_delta[:remove_foreign_key].each do |index_name, _foreign_key|
43
+ results << alter_remove_foreign_key_sql(table_name, index_name)
44
+ end
45
+ table_delta[:add_foreign_key].each do |_index_name, foreign_key|
46
+ results << alter_add_foreign_key_sql(table_name, foreign_key)
47
+ end
48
+ unless table_delta[:change_table_option].empty?
49
+ results << alter_change_table_sql(table_name, table_delta[:change_table_option])
50
+ end
51
+ end
52
+ results
53
+ end
54
+
55
+ def alter_add_column_sql(table_name, column)
56
+ %(ALTER TABLE `#{table_name}` ADD COLUMN #{create_column_sql(column, output_primary_key: true)};)
57
+ end
58
+
59
+ def alter_remove_column_sql(table_name, column)
60
+ %(ALTER TABLE `#{table_name}` DROP COLUMN `#{column.column_name}`;)
61
+ end
62
+
63
+ def alter_change_column_sql(table_name, column_name, change_column_option, to_table)
64
+ column = to_table[table_name].columns[column_name]
65
+ column.options.merge!(after: change_column_option[:after]) unless change_column_option[:after].nil?
66
+ %(ALTER TABLE `#{table_name}` MODIFY COLUMN #{create_column_sql(column, output_primary_key: true)};)
67
+ end
68
+
69
+ def alter_change_table_sql(table_name, change_table_option)
70
+ sql = "ALTER TABLE `#{table_name}`"
71
+ change_table_option.each do |key, value|
72
+ if QUOTE_OPTION.include?(key)
73
+ sql += " #{OPTION_MAPPING[key]}='#{value}'"
74
+ else
75
+ sql += " #{OPTION_MAPPING[key]}=#{value}"
76
+ end
77
+ end
78
+ sql += ';'
79
+ sql
80
+ end
81
+
82
+ def alter_remove_index_sql(table_name, index_name)
83
+ %(DROP INDEX `#{index_name}` ON `#{table_name}`;)
84
+ end
85
+
86
+ def alter_add_index_sql(table_name, index)
87
+ sql = 'CREATE'
88
+ sql += ' UNIQUE' if index.options[:unique]
89
+ sql += " INDEX `#{index.index_name}` ON `#{table_name}`(#{index.index_columns.join(',')});"
90
+ sql
91
+ end
92
+
93
+ def alter_remove_foreign_key_sql(table_name, index_name)
94
+ sql = %(ALTER TABLE `#{table_name}` DROP FOREIGN KEY `#{index_name}`;\n)
95
+ sql += alter_remove_index_sql(table_name, index_name)
96
+ sql
97
+ end
98
+
99
+ def alter_add_foreign_key_sql(table_name, foreign_key)
100
+ sql = %(ALTER TABLE `#{table_name}` ADD CONSTRAINT `#{foreign_key.key_name}` FOREIGN KEY )
101
+ sql += "(#{[foreign_key.from_columns].join(',')}) REFERENCES `#{foreign_key.to_table}`"
102
+ sql += "(#{[foreign_key.to_columns].join(',')});"
103
+ sql
104
+ end
105
+
106
+ def create_table_sqls(delta)
107
+ delta[:add_table].map do |table_name, table|
108
+ column_sql = (create_table_column_sql(table) << create_table_index_sql(table))
109
+ .flatten
110
+ .reject(&:empty?)
111
+ .join(",\n ")
112
+ <<-SQL
113
+ CREATE TABLE `#{table_name}` (
114
+ #{column_sql}
115
+ ) #{create_table_option_sql(table)};
116
+ SQL
117
+ end
118
+ end
119
+
120
+ def drop_table_sqls(delta)
121
+ delta[:remove_table].map do |table_name, _|
122
+ <<-SQL
123
+ DROP TABLE `#{table_name}`;
124
+ SQL
125
+ end
126
+ end
127
+
128
+ def create_table_column_sql(table)
129
+ table.columns.values.map do |column|
130
+ create_column_sql(column)
131
+ end
132
+ end
133
+
134
+ def create_column_sql(column, output_primary_key: false)
135
+ sql = "`#{column.column_name}`"
136
+ sql += " #{column.type}"
137
+ sql += "(#{column.options[:limit]})" unless column.options[:limit].nil?
138
+ if column.options[:precision] && column.options[:scale]
139
+ sql += "(#{column.options[:precision]}, #{column.options[:scale]})"
140
+ end
141
+ if column.options[:character_set]
142
+ sql += " CHARACTER SET #{column.options[:character_set]}"
143
+ end
144
+ if column.options[:collate]
145
+ sql += " COLLATE #{column.options[:collate]}"
146
+ end
147
+ if column.options[:null]
148
+ sql += ' DEFAULT NULL' unless column.options[:default]
149
+ else
150
+ sql += ' NOT NULL'
151
+ end
152
+ if column.options[:primary_key] && output_primary_key
153
+ sql += ' PRIMARY KEY'
154
+ end
155
+ if column.options[:default]
156
+ sql += " DEFAULT '#{column.options[:default]}'"
157
+ end
158
+ if column.options[:comment]
159
+ sql += " COMMENT '#{column.options[:comment]}'"
160
+ end
161
+ if column.options[:extra]
162
+ sql += " #{column.options[:extra].upcase}"
163
+ end
164
+ if column.options.keys.include?(:after)
165
+ if column.options[:after].nil?
166
+ sql += ' FIRST'
167
+ else
168
+ sql += " AFTER `#{column.options[:after]}`"
169
+ end
170
+ end
171
+ sql
172
+ end
173
+
174
+ def create_table_index_sql(table)
175
+ pkeys = table.columns.select { |_k, v| v.options[:primary_key] }
176
+ unique_keys = table.indexes.values.select { |v| v.options[:unique] }
177
+ index_keys = table.indexes.values.reject { |v| v.options[:unique] }
178
+ foreign_keys = table.foreign_keys.values
179
+ results = []
180
+ unless pkeys.empty?
181
+ results << %(PRIMARY KEY (#{pkeys.keys.map { |v| "`#{v}`" }.join(',')}))
182
+ end
183
+ results << unique_keys.map do |uk|
184
+ %(UNIQUE KEY `#{uk.index_name}` (#{uk.index_columns.map { |v| "`#{v}`" }.join(',')}))
185
+ end
186
+ results << index_keys.map do |ik|
187
+ %(KEY `#{ik.index_name}` (#{ik.index_columns.map { |v| "`#{v}`" }.join(',')}))
188
+ end
189
+ results << foreign_keys.map do |fk|
190
+ sql = %(CONSTRAINT `#{fk.key_name}` FOREIGN KEY)
191
+ sql += %( (#{[fk.from_columns].flatten.map { |v| "`#{v}`" }.join(',')}))
192
+ sql += %( REFERENCES `#{fk.to_table}` (#{[fk.to_columns].flatten.map { |v| "`#{v}`" }.join(',')}))
193
+ sql
194
+ end
195
+ results
196
+ end
197
+
198
+ def create_table_option_sql(table)
199
+ table.table_options.map do |k, v|
200
+ key = OPTION_MAPPING[k] || k.to_s.upcase
201
+ if QUOTE_OPTION.include?(k)
202
+ "#{key}=\"#{v}\""
203
+ else
204
+ "#{key}=#{v}"
205
+ end
206
+ end.join(' ')
207
+ end
208
+ end