convergence 1.0.6 → 1.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +35 -4
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +46 -0
  5. data/CONTRIBUTING.md +80 -0
  6. data/Gemfile.lock +23 -8
  7. data/README.md +131 -1
  8. data/Rakefile +45 -2
  9. data/convergence.gemspec +3 -1
  10. data/lib/convergence/cli.rb +10 -2
  11. data/lib/convergence/command/apply.rb +38 -9
  12. data/lib/convergence/command/dryrun.rb +5 -5
  13. data/lib/convergence/command/export.rb +19 -1
  14. data/lib/convergence/command.rb +6 -0
  15. data/lib/convergence/database_connector/postgres_connector.rb +25 -0
  16. data/lib/convergence/database_connector/sqlite_connector.rb +22 -0
  17. data/lib/convergence/database_connector.rb +6 -0
  18. data/lib/convergence/default_parameter/mysql_default_parameter.rb +1 -1
  19. data/lib/convergence/default_parameter/postgres_default_parameter.rb +57 -0
  20. data/lib/convergence/default_parameter/sqlite_default_parameter.rb +57 -0
  21. data/lib/convergence/default_parameter.rb +6 -0
  22. data/lib/convergence/diff.rb +9 -2
  23. data/lib/convergence/dsl.rb +18 -3
  24. data/lib/convergence/dumper/mysql_schema_dumper.rb +37 -9
  25. data/lib/convergence/dumper/postgres_schema_dumper.rb +193 -0
  26. data/lib/convergence/dumper/sqlite_schema_dumper.rb +132 -0
  27. data/lib/convergence/dumper.rb +113 -0
  28. data/lib/convergence/sql_generator/mysql_generator.rb +18 -3
  29. data/lib/convergence/sql_generator/postgres_generator.rb +256 -0
  30. data/lib/convergence/sql_generator/sqlite_generator.rb +178 -0
  31. data/lib/convergence/version.rb +1 -1
  32. data/spec/config/spec_database.yml +12 -0
  33. data/spec/convergence/config_spec.rb +75 -0
  34. data/spec/convergence/diff_spec.rb +125 -45
  35. data/spec/convergence/dsl_spec.rb +26 -0
  36. data/spec/convergence/dumper/mysql_schema_dumper_spec.rb +25 -2
  37. data/spec/convergence/dumper/postgres_schema_dumper_spec.rb +103 -0
  38. data/spec/convergence/dumper/sqlite_schema_dumper_spec.rb +85 -0
  39. data/spec/convergence/dumper_spec.rb +110 -16
  40. data/spec/convergence/foreign_key_spec.rb +33 -0
  41. data/spec/convergence/index_spec.rb +52 -0
  42. data/spec/convergence/pretty_diff_spec.rb +85 -0
  43. data/spec/convergence/table_spec.rb +23 -0
  44. data/spec/fixtures/add_table_with_enum_set.schema +5 -0
  45. data/spec/fixtures/change_table_comment_to_paper.schema +2 -0
  46. data/spec/fixtures/execute_raw_sql.schema +28 -0
  47. data/spec/fixtures/postgres/add_columns_to_paper.schema +29 -0
  48. data/spec/fixtures/postgres/add_table.schema +32 -0
  49. data/spec/fixtures/postgres/change_comment_columns_to_paper.schema +28 -0
  50. data/spec/fixtures/postgres/change_table_comment_to_paper.schema +28 -0
  51. data/spec/fixtures/postgres/drop_foreign_key.schema +25 -0
  52. data/spec/fixtures/postgres/drop_table.schema +19 -0
  53. data/spec/fixtures/postgres/remove_columns_to_paper.schema +27 -0
  54. data/spec/fixtures/postgres_test_db.sql +41 -0
  55. data/spec/fixtures/sqlite/add_columns_to_paper.schema +29 -0
  56. data/spec/fixtures/sqlite/add_table.schema +32 -0
  57. data/spec/fixtures/sqlite/change_columns_to_paper.schema +28 -0
  58. data/spec/fixtures/sqlite/drop_foreign_key.schema +25 -0
  59. data/spec/fixtures/sqlite/drop_table.schema +19 -0
  60. data/spec/fixtures/sqlite/remove_columns_to_paper.schema +27 -0
  61. data/spec/fixtures/sqlite_test_db.sql +31 -0
  62. data/spec/fixtures/test_db.sql +10 -0
  63. data/spec/integrations/command_diff.rb +35 -0
  64. data/spec/integrations/command_dryrun.rb +37 -2
  65. data/spec/integrations/command_export.rb +28 -0
  66. data/spec/postgres_integrations/command_dryrun.rb +79 -0
  67. data/spec/spec_helper.rb +13 -0
  68. data/spec/sqlite_integrations/command_dryrun.rb +70 -0
  69. metadata +96 -8
@@ -0,0 +1,178 @@
1
+ require 'convergence/sql_generator'
2
+
3
+ # SQLite's ALTER TABLE support is intentionally limited: it can add/drop columns
4
+ # and create/drop indexes, but it cannot change a column's type/null/default,
5
+ # nor add/remove a foreign key or primary key on an existing table. Doing any of
6
+ # those requires SQLite's official "rebuild the table" procedure (create a new
7
+ # table, copy the data over, drop the old one, rename), which this adapter does
8
+ # not implement yet -- it raises NotImplementedError instead of generating SQL
9
+ # that SQLite would reject.
10
+ class SQLGenerator::SqliteGenerator < SQLGenerator
11
+ UNSUPPORTED_TYPES = [:enum, :set].freeze
12
+
13
+ attr_reader :original_table
14
+
15
+ def generate(to_table, delta, original_table, safe_migration: false)
16
+ @original_table = original_table
17
+ sqls = []
18
+ sqls << change_table_sql(to_table, delta)
19
+ sqls << (safe_migration ? [] : drop_table_sqls(delta))
20
+ sqls << create_table_sqls(delta)
21
+ sqls.reject!(&:empty?)
22
+ sqls.join("\n")
23
+ end
24
+
25
+ private
26
+
27
+ def change_table_sql(to_table, delta)
28
+ change_table = delta[:change_table]
29
+ results = []
30
+ change_table.each do |table_name, table_delta|
31
+ unless table_delta[:remove_foreign_key].empty?
32
+ fail unsupported_alter_error(table_name, 'removing a foreign key')
33
+ end
34
+ unless table_delta[:add_foreign_key].empty?
35
+ fail unsupported_alter_error(table_name, 'adding a foreign key')
36
+ end
37
+ unless table_delta[:change_column].empty?
38
+ columns = table_delta[:change_column].keys.join(', ')
39
+ fail unsupported_alter_error(table_name, "changing column(s) #{columns} (type/null/default/primary key)")
40
+ end
41
+ table_delta[:remove_index].each do |index_name, _index|
42
+ results << alter_remove_index_sql(index_name)
43
+ end
44
+ unless table_delta[:remove_column].empty?
45
+ results << alter_remove_columns_sql(table_name, table_delta[:remove_column].values)
46
+ end
47
+ unless table_delta[:add_column].empty?
48
+ results << alter_add_columns_sql(table_name, table_delta[:add_column].values)
49
+ end
50
+ table_delta[:add_index].each do |_index_name, index|
51
+ results << alter_add_index_sql(table_name, index)
52
+ end
53
+ # SQLite has no table-level options comparable to MySQL's engine/comment/
54
+ # charset, so any detected change_table_option diff is silently ignored.
55
+ end
56
+ results << '' unless results.empty?
57
+ results
58
+ end
59
+
60
+ def unsupported_alter_error(table_name, action)
61
+ NotImplementedError.new(
62
+ "SQLite adapter does not support #{action} on an existing table (`#{table_name}`) via ALTER TABLE. " \
63
+ 'This requires rebuilding the table (create new, copy data, drop old, rename), which is not supported yet.'
64
+ )
65
+ end
66
+
67
+ def alter_add_columns_sql(table_name, columns)
68
+ columns.map { |column| %(ALTER TABLE "#{table_name}" ADD COLUMN #{create_column_sql(column)};) }.join("\n")
69
+ end
70
+
71
+ def alter_remove_columns_sql(table_name, columns)
72
+ columns.map { |column| %(ALTER TABLE "#{table_name}" DROP COLUMN "#{column.column_name}";) }.join("\n")
73
+ end
74
+
75
+ def alter_remove_index_sql(index_name)
76
+ %(DROP INDEX "#{index_name}";)
77
+ end
78
+
79
+ def alter_add_index_sql(table_name, index)
80
+ sql = 'CREATE'
81
+ sql += ' UNIQUE' if index.options[:unique]
82
+ sql += %( INDEX "#{index.index_name}" ON "#{table_name}" (#{quoted_index_columns(index).join(',')});)
83
+ sql
84
+ end
85
+
86
+ def create_table_sqls(delta)
87
+ delta[:add_table].map do |table_name, table|
88
+ column_sql = (create_table_column_sql(table) << create_table_constraint_sql(table))
89
+ .flatten
90
+ .reject(&:empty?)
91
+ .join(",\n ")
92
+ sql = <<-SQL
93
+ CREATE TABLE "#{table_name}" (
94
+ #{column_sql}
95
+ );
96
+ SQL
97
+ index_sqls = table.indexes.values.map { |index| alter_add_index_sql(table_name, index) }
98
+ ([sql.strip] + index_sqls).join("\n")
99
+ end
100
+ end
101
+
102
+ def drop_table_sqls(delta)
103
+ delta[:remove_table].map do |table_name, _|
104
+ %(DROP TABLE "#{table_name}";)
105
+ end
106
+ end
107
+
108
+ def create_table_column_sql(table)
109
+ table.columns.values.map do |column|
110
+ create_column_sql(column)
111
+ end
112
+ end
113
+
114
+ def create_column_sql(column)
115
+ fail NotImplementedError.new("#{column.type} is not supported on the SQLite adapter yet") if UNSUPPORTED_TYPES.include?(column.type)
116
+ return %("#{column.column_name}" INTEGER PRIMARY KEY AUTOINCREMENT) if auto_increment?(column)
117
+ sql = %("#{column.column_name}")
118
+ sql += " #{sqlite_column_type(column)}"
119
+ if column.options[:primary_key]
120
+ sql += ' PRIMARY KEY'
121
+ end
122
+ sql += ' NOT NULL' unless column.options[:null]
123
+ if column.options[:default]
124
+ sql += " DEFAULT #{quote_default_expression(column.options[:default])}"
125
+ end
126
+ sql
127
+ end
128
+
129
+ def sqlite_column_type(column)
130
+ type = column.type.to_s
131
+ if column.type == :decimal && column.options[:precision] && column.options[:scale]
132
+ "#{type}(#{column.options[:precision]}, #{column.options[:scale]})"
133
+ elsif column.options[:limit] && !column.options[:limit].to_s.empty?
134
+ "#{type}(#{column.options[:limit]})"
135
+ else
136
+ type
137
+ end
138
+ end
139
+
140
+ def auto_increment?(column)
141
+ extra = column.options[:extra]
142
+ !extra.nil? && extra.to_s.upcase.include?('AUTO_INCREMENT')
143
+ end
144
+
145
+ # A single-column INTEGER PRIMARY KEY AUTOINCREMENT already declares itself as
146
+ # the primary key inline, so it must not also appear in a table-level
147
+ # PRIMARY KEY (...) clause.
148
+ def create_table_constraint_sql(table)
149
+ pkeys = table.columns.reject { |_k, v| auto_increment?(v) }.select { |_k, v| v.options[:primary_key] }
150
+ foreign_keys = table.foreign_keys.values
151
+ results = []
152
+ unless pkeys.empty?
153
+ results << %(PRIMARY KEY (#{pkeys.keys.map { |v| %("#{v}") }.join(',')}))
154
+ end
155
+ results << foreign_keys.map { |fk| foreign_key_constraint_sql(fk) }
156
+ results
157
+ end
158
+
159
+ def foreign_key_constraint_sql(foreign_key)
160
+ sql = %(CONSTRAINT "#{foreign_key.key_name}" FOREIGN KEY )
161
+ sql += "(#{[foreign_key.from_columns].flatten.map { |v| %("#{v}") }.join(',')}) "
162
+ sql += %(REFERENCES "#{foreign_key.to_table}" )
163
+ sql += "(#{[foreign_key.to_columns].flatten.map { |v| %("#{v}") }.join(',')})"
164
+ sql
165
+ end
166
+
167
+ def quoted_index_columns(index)
168
+ [index.index_columns].flatten.map { |v| %("#{v}") }
169
+ end
170
+
171
+ def quote_default_expression(value)
172
+ if value.is_a?(Proc)
173
+ value.call
174
+ else
175
+ %('#{value.to_s.gsub("'", "''")}')
176
+ end
177
+ end
178
+ end
@@ -1,3 +1,3 @@
1
1
  module Convergence
2
- VERSION = '1.0.6'
2
+ VERSION = '1.1.0'
3
3
  end
@@ -5,3 +5,15 @@ mysql:
5
5
  port: 3306
6
6
  username: root
7
7
  password: root
8
+
9
+ postgresql:
10
+ adapter: postgresql
11
+ database: convergence_test_db
12
+ host: 127.0.0.1
13
+ port: 5432
14
+ username: postgres
15
+ password: postgres
16
+
17
+ sqlite3:
18
+ adapter: sqlite3
19
+ database: sqlite_test_db.sqlite3
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+ require 'convergence/config'
3
+ require 'tempfile'
4
+
5
+ describe Convergence::Config do
6
+ describe '#initialize' do
7
+ it 'sets the known attributes' do
8
+ config = Convergence::Config.new(
9
+ adapter: 'mysql2',
10
+ database: 'my_db',
11
+ host: '127.0.0.1',
12
+ port: 3306,
13
+ username: 'root',
14
+ password: 'secret'
15
+ )
16
+ expect(config.adapter).to eq('mysql2')
17
+ expect(config.database).to eq('my_db')
18
+ expect(config.host).to eq('127.0.0.1')
19
+ expect(config.port).to eq(3306)
20
+ expect(config.username).to eq('root')
21
+ expect(config.password).to eq('secret')
22
+ end
23
+
24
+ it 'ignores unknown attributes instead of raising' do
25
+ expect { Convergence::Config.new(adapter: 'mysql2', unknown_key: 'value') }.not_to raise_error
26
+ end
27
+
28
+ it 'ignores nil-valued attributes' do
29
+ config = Convergence::Config.new(adapter: 'mysql2', database: nil)
30
+ expect(config.database).to be_nil
31
+ end
32
+
33
+ context 'when the adapter is mysql/mysql2' do
34
+ it 'builds a nested MySQL SSL config object' do
35
+ config = Convergence::Config.new(adapter: 'mysql2', sslca: '/path/to/ca.pem', sslverify: true)
36
+ expect(config.mysql).to be_a(Convergence::Config::MySQL)
37
+ expect(config.mysql.ssl_options).to eq(sslca: '/path/to/ca.pem', sslverify: true)
38
+ end
39
+
40
+ it 'omits unset SSL options from ssl_options' do
41
+ config = Convergence::Config.new(adapter: 'mysql2')
42
+ expect(config.mysql.ssl_options).to eq({})
43
+ end
44
+ end
45
+
46
+ context 'when the adapter is not mysql/mysql2' do
47
+ it 'does not build a MySQL SSL config object' do
48
+ config = Convergence::Config.new(adapter: 'postgresql')
49
+ expect(config.mysql).to be_nil
50
+ end
51
+ end
52
+ end
53
+
54
+ describe '.load' do
55
+ it 'parses attributes from a YAML file' do
56
+ Tempfile.create(['database', '.yml']) do |file|
57
+ file.write("adapter: mysql2\ndatabase: my_db\nhost: 127.0.0.1\n")
58
+ file.flush
59
+ config = Convergence::Config.load(file.path)
60
+ expect(config.adapter).to eq('mysql2')
61
+ expect(config.database).to eq('my_db')
62
+ expect(config.host).to eq('127.0.0.1')
63
+ end
64
+ end
65
+
66
+ it 'evaluates ERB before parsing the YAML' do
67
+ Tempfile.create(['database', '.yml']) do |file|
68
+ file.write("adapter: mysql2\ndatabase: <%= 'interpolated_' + 'db' %>\n")
69
+ file.flush
70
+ config = Convergence::Config.load(file.path)
71
+ expect(config.database).to eq('interpolated_db')
72
+ end
73
+ end
74
+ end
75
+ end
@@ -104,6 +104,7 @@ describe Convergence::DSL do
104
104
  let(:table_to) do
105
105
  Convergence::Table.new('table1').tap do |t|
106
106
  t.int('id')
107
+ t.varchar('name', limit: 200, null: false)
107
108
  end
108
109
  end
109
110
 
@@ -135,130 +136,209 @@ describe Convergence::DSL do
135
136
  end
136
137
 
137
138
  describe '#diff_table' do
138
- let(:table_from) do
139
- Convergence::Table.new('table1').tap do |t|
140
- t.int('id', primary_key: true)
141
- t.varchar('name', limit: 200, null: false)
139
+ subject(:results) { Convergence::Diff.new(**diff_options).diff_table(table_from, table_to) }
140
+ let(:diff_options) { {} }
141
+
142
+ context 'change auto_increment table option' do
143
+ let(:table_from) do
144
+ Convergence::Table.new('table1').tap do |t|
145
+ t.int('id', primary_key: true)
146
+ t.table_options = { auto_increment: 1 }
147
+ end
148
+ end
149
+ let(:table_to) do
150
+ Convergence::Table.new('table1').tap do |t|
151
+ t.int('id', primary_key: true)
152
+ t.table_options = { auto_increment: 5000 }
153
+ end
154
+ end
155
+
156
+ it 'detects the auto_increment change by default' do
157
+ expect(results[:change_table_option][:auto_increment]).to eq(5000)
158
+ end
159
+
160
+ context 'when ignore_auto_increment is enabled' do
161
+ let(:diff_options) { { ignore_auto_increment: true } }
142
162
 
143
- t.index('name')
144
- t.foreign_key('id', reference: 'ref_tables', reference_column: 'ref_id')
163
+ it 'ignores the auto_increment change' do
164
+ expect(results[:change_table_option]).not_to have_key(:auto_increment)
165
+ end
145
166
  end
146
167
  end
147
168
 
148
169
  context 'change column options' do
170
+ let(:table_from) do
171
+ Convergence::Table.new('table1').tap do |t|
172
+ t.int('id', primary_key: true)
173
+ t.varchar('name', limit: 200, null: false)
174
+ t.datetime('created_at', default: -> { "CURRENT_TIMESTAMP" })
175
+ end
176
+ end
149
177
  let(:table_to) do
150
178
  Convergence::Table.new('table1').tap do |t|
151
179
  t.int('id', primary_key: true)
152
- t.varchar('name', limit: 300, null: true, unsigned: true)
153
-
154
- t.index('name')
180
+ t.varchar('name', limit: 300, null: true, unsigned: true) # option changed
181
+ t.datetime('created_at', default: -> { "CURRENT_TIMESTAMP" }) # option[:default] not changed
155
182
  end
156
183
  end
157
184
 
158
185
  it do
159
- results = Convergence::Diff.new.diff_table(table_from, table_to)
160
186
  expect(results[:change_column]['name']).not_to be_nil
161
187
  expect(results[:change_column]['name'][:limit]).to eq('300')
162
188
  expect(results[:change_column]['name'][:null]).to eq('true')
163
189
  expect(results[:change_column]['name'][:unsigned]).to eq('true')
190
+ expect(results[:change_column]['created_at']).to be_nil
191
+ end
192
+ end
193
+
194
+ context 'change enum/set column values' do
195
+ let(:table_from) do
196
+ Convergence::Table.new('table1').tap do |t|
197
+ t.int('id', primary_key: true)
198
+ t.enum('status', values: %w(Active Inactive))
199
+ end
200
+ end
201
+
202
+ context 'when values are identical' do
203
+ let(:table_to) do
204
+ Convergence::Table.new('table1').tap do |t|
205
+ t.int('id', primary_key: true)
206
+ t.enum('status', values: %w(Active Inactive))
207
+ end
208
+ end
209
+
210
+ it 'does not detect a change' do
211
+ expect(results[:change_column]['status']).to be_nil
212
+ end
213
+ end
214
+
215
+ context 'when only the letter case differs' do
216
+ let(:table_to) do
217
+ Convergence::Table.new('table1').tap do |t|
218
+ t.int('id', primary_key: true)
219
+ t.enum('status', values: %w(active inactive))
220
+ end
221
+ end
222
+
223
+ it 'detects the change since values are case-sensitive' do
224
+ expect(results[:change_column]['status']).not_to be_nil
225
+ expect(results[:change_column]['status'][:values]).to eq('["active", "inactive"]')
226
+ end
164
227
  end
165
228
  end
166
229
 
167
230
  context 'change column order' do
231
+ let(:table_from) do
232
+ Convergence::Table.new('table1').tap do |t|
233
+ t.int('id', primary_key: true)
234
+ t.varchar('name', limit: 200, null: false)
235
+ end
236
+ end
168
237
  let(:table_to) do
169
238
  Convergence::Table.new('table1').tap do |t|
170
- t.varchar('name', limit: 300, null: true)
239
+ t.varchar('name', limit: 200, null: false)
171
240
  t.int('id', primary_key: true)
172
-
173
- t.index('name')
174
241
  end
175
242
  end
176
243
 
177
244
  it do
178
- results = Convergence::Diff.new.diff_table(table_from, table_to)
179
- expect(results[:change_column]['name']).not_to be_nil
245
+ expect(results[:change_column]['name']).to be_nil
180
246
  expect(results[:change_column]['id']).not_to be_nil
181
247
  expect(results[:change_column]['id'][:after]).to eq('name')
182
248
  end
183
249
  end
184
250
 
185
251
  context 'remove index' do
252
+ let(:table_from) do
253
+ Convergence::Table.new('table1').tap do |t|
254
+ t.int('id', primary_key: true)
255
+ t.varchar('name', limit: 200, null: false)
256
+
257
+ t.index('name')
258
+ end
259
+ end
186
260
  let(:table_to) do
187
261
  Convergence::Table.new('table1').tap do |t|
188
262
  t.int('id', primary_key: true)
189
- t.varchar('name', limit: 300, null: true)
263
+ t.varchar('name', limit: 200, null: false)
190
264
  end
191
265
  end
192
266
 
193
- it do
194
- results = Convergence::Diff.new.diff_table(table_from, table_to)
195
- expect(results[:remove_index].values.first.index_columns).to eq(['name'])
196
- end
267
+ it { expect(results[:remove_index].values.first.index_columns).to eq(['name']) }
197
268
  end
198
269
 
199
270
  context 'add index' do
271
+ let(:table_from) do
272
+ Convergence::Table.new('table1').tap do |t|
273
+ t.int('id', primary_key: true)
274
+ t.varchar('name', limit: 200, null: false)
275
+ end
276
+ end
200
277
  let(:table_to) do
201
278
  Convergence::Table.new('table1').tap do |t|
202
279
  t.int('id', primary_key: true)
203
- t.varchar('name', limit: 300, null: true)
280
+ t.varchar('name', limit: 200, null: false)
204
281
 
205
- t.index('id')
206
282
  t.index('name')
207
283
  end
208
284
  end
209
285
 
210
- it do
211
- results = Convergence::Diff.new.diff_table(table_from, table_to)
212
- expect(results[:add_index].values.first.index_columns).to eq(['id'])
213
- end
286
+ it { expect(results[:add_index].values.first.index_columns).to eq(['name']) }
214
287
  end
215
288
 
216
289
  context 'remove foreign key' do
217
- let(:table_to) do
290
+ let(:table_from) do
218
291
  Convergence::Table.new('table1').tap do |t|
219
292
  t.int('id', primary_key: true)
220
- t.varchar('name', limit: 300, null: true)
293
+ t.varchar('name', limit: 200, null: false)
221
294
 
222
- t.index('name')
295
+ t.foreign_key('id', reference: 'ref_tables', reference_column: 'ref_id')
223
296
  end
224
297
  end
225
-
226
- it do
227
- results = Convergence::Diff.new.diff_table(table_from, table_to)
228
- expect(results[:remove_foreign_key].values.first.from_columns).to eq(['id'])
298
+ let(:table_to) do
299
+ Convergence::Table.new('table1').tap do |t|
300
+ t.int('id', primary_key: true)
301
+ t.varchar('name', limit: 200, null: false)
302
+ end
229
303
  end
304
+
305
+ it { expect(results[:remove_foreign_key].values.first.from_columns).to eq(['id']) }
230
306
  end
231
307
 
232
308
  context 'add foreign key' do
309
+ let(:table_from) do
310
+ Convergence::Table.new('table1').tap do |t|
311
+ t.int('id', primary_key: true)
312
+ t.varchar('name', limit: 200, null: false)
313
+ end
314
+ end
233
315
  let(:table_to) do
234
316
  Convergence::Table.new('table1').tap do |t|
235
317
  t.int('id', primary_key: true)
236
- t.varchar('name', limit: 300, null: true)
318
+ t.varchar('name', limit: 200, null: false)
237
319
 
238
- t.index('name')
239
320
  t.foreign_key('id', reference: 'ref_tables', reference_column: 'ref_id')
240
- t.foreign_key('id2', reference: 'ref_tables2', reference_column: 'ref_id2')
241
321
  end
242
322
  end
243
323
 
244
- it do
245
- results = Convergence::Diff.new.diff_table(table_from, table_to)
246
- expect(results[:add_foreign_key].values.first.from_columns).to eq(['id2'])
247
- end
324
+ it { expect(results[:add_foreign_key].values.first.from_columns).to eq(['id']) }
248
325
  end
249
326
 
250
327
  context 'change table options' do
328
+ let(:table_from) do
329
+ Convergence::Table.new('table1').tap do |t|
330
+ t.int('id', primary_key: true)
331
+ t.varchar('name', limit: 200, null: false)
332
+ end
333
+ end
251
334
  let(:table_to) do
252
335
  Convergence::Table.new('table1', engine: 'MyISAM').tap do |t|
253
- t.varchar('name', limit: 300, null: true)
254
336
  t.int('id', primary_key: true)
337
+ t.varchar('name', limit: 200, null: false)
255
338
  end
256
339
  end
257
340
 
258
- it do
259
- results = Convergence::Diff.new.diff_table(table_from, table_to)
260
- expect(results[:change_table_option][:engine]).to eq('MyISAM')
261
- end
341
+ it { expect(results[:change_table_option][:engine]).to eq('MyISAM') }
262
342
  end
263
343
  end
264
344
  end
@@ -89,4 +89,30 @@ describe Convergence::DSL do
89
89
  end
90
90
  end
91
91
  end
92
+
93
+ describe '#parse_dsl' do
94
+ let(:dsl_with_execute) do
95
+ <<-DSL
96
+ create_table "users" do |t|
97
+ t.int "id", primary_key: true, extra: "auto_increment"
98
+ end
99
+
100
+ execute "CREATE OR REPLACE VIEW active_users AS SELECT * FROM users"
101
+ execute "GRANT SELECT ON users TO readonly"
102
+ DSL
103
+ end
104
+
105
+ subject { Convergence::DSL.parse_dsl(dsl_with_execute, '') }
106
+
107
+ it 'should be able to parse tables' do
108
+ expect(subject.tables['users']).not_to be_nil
109
+ end
110
+
111
+ it 'should be able to collect raw sql statements in order' do
112
+ expect(subject.raw_sqls).to eq([
113
+ 'CREATE OR REPLACE VIEW active_users AS SELECT * FROM users',
114
+ 'GRANT SELECT ON users TO readonly'
115
+ ])
116
+ end
117
+ end
92
118
  end
@@ -37,14 +37,17 @@ describe Convergence::Dumper::MysqlSchemaDumper do
37
37
  expect(papers.columns['title1']).not_to be_nil
38
38
  expect(papers.columns['title2']).not_to be_nil
39
39
  expect(papers.columns['description']).not_to be_nil
40
+ expect(papers.columns['edition_number']).not_to be_nil
41
+ expect(papers.columns['published_at']).not_to be_nil
40
42
  end
41
43
 
42
44
  it 'should be dump columns in the correct order' do
43
45
  papers = subject['papers']
44
- expect(papers.columns.keys).to eq(%w(id slug title1 title2 description))
46
+ expect(papers.columns.keys)
47
+ .to eq(%w(id slug title1 title2 description edition_number published_at))
45
48
  end
46
49
 
47
- describe 'table options' do
50
+ describe 'table column options' do
48
51
  it 'should be dump primary key' do
49
52
  expect(subject['papers'].columns['id'].options[:primary_key]).to be_truthy
50
53
  end
@@ -69,6 +72,13 @@ describe Convergence::Dumper::MysqlSchemaDumper do
69
72
  it 'should be dump unsigned definition' do
70
73
  expect(subject['authors'].columns['age'].options[:unsigned]).to be_truthy
71
74
  end
75
+
76
+ it 'should be dump default' do
77
+ expect(subject['papers'].columns['edition_number'].options[:default]).to eq "0"
78
+ expect(subject['papers'].columns['published_at'].options[:default])
79
+ .to be_a(Proc)
80
+ .and have_attributes(call: "CURRENT_TIMESTAMP")
81
+ end
72
82
  end
73
83
  end
74
84
 
@@ -96,6 +106,19 @@ describe Convergence::Dumper::MysqlSchemaDumper do
96
106
  end
97
107
  end
98
108
 
109
+ describe 'enum/set columns' do
110
+ it 'should be dump enum values' do
111
+ expect(subject['enum_set_samples'].columns['status'].options[:values])
112
+ .to eq(%w(active inactive pending))
113
+ expect(subject['enum_set_samples'].columns['status'].options[:default]).to eq('active')
114
+ end
115
+
116
+ it 'should be dump set values, including values containing quotes' do
117
+ expect(subject['enum_set_samples'].columns['flags'].options[:values])
118
+ .to eq(['a', 'b', "it's tricky"])
119
+ end
120
+ end
121
+
99
122
  describe 'foreign keys' do
100
123
  it do
101
124
  foreign_key = subject['paper_authors'].foreign_keys['paper_authors_author_id_fk']