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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +35 -4
- data/.gitignore +1 -0
- data/CHANGELOG.md +46 -0
- data/CONTRIBUTING.md +80 -0
- data/Gemfile.lock +23 -8
- data/README.md +131 -1
- data/Rakefile +45 -2
- data/convergence.gemspec +3 -1
- data/lib/convergence/cli.rb +10 -2
- data/lib/convergence/command/apply.rb +38 -9
- data/lib/convergence/command/dryrun.rb +5 -5
- data/lib/convergence/command/export.rb +19 -1
- data/lib/convergence/command.rb +6 -0
- data/lib/convergence/database_connector/postgres_connector.rb +25 -0
- data/lib/convergence/database_connector/sqlite_connector.rb +22 -0
- data/lib/convergence/database_connector.rb +6 -0
- data/lib/convergence/default_parameter/mysql_default_parameter.rb +1 -1
- data/lib/convergence/default_parameter/postgres_default_parameter.rb +57 -0
- data/lib/convergence/default_parameter/sqlite_default_parameter.rb +57 -0
- data/lib/convergence/default_parameter.rb +6 -0
- data/lib/convergence/diff.rb +9 -2
- data/lib/convergence/dsl.rb +18 -3
- data/lib/convergence/dumper/mysql_schema_dumper.rb +37 -9
- data/lib/convergence/dumper/postgres_schema_dumper.rb +193 -0
- data/lib/convergence/dumper/sqlite_schema_dumper.rb +132 -0
- data/lib/convergence/dumper.rb +113 -0
- data/lib/convergence/sql_generator/mysql_generator.rb +18 -3
- data/lib/convergence/sql_generator/postgres_generator.rb +256 -0
- data/lib/convergence/sql_generator/sqlite_generator.rb +178 -0
- data/lib/convergence/version.rb +1 -1
- data/spec/config/spec_database.yml +12 -0
- data/spec/convergence/config_spec.rb +75 -0
- data/spec/convergence/diff_spec.rb +125 -45
- data/spec/convergence/dsl_spec.rb +26 -0
- data/spec/convergence/dumper/mysql_schema_dumper_spec.rb +25 -2
- data/spec/convergence/dumper/postgres_schema_dumper_spec.rb +103 -0
- data/spec/convergence/dumper/sqlite_schema_dumper_spec.rb +85 -0
- data/spec/convergence/dumper_spec.rb +110 -16
- data/spec/convergence/foreign_key_spec.rb +33 -0
- data/spec/convergence/index_spec.rb +52 -0
- data/spec/convergence/pretty_diff_spec.rb +85 -0
- data/spec/convergence/table_spec.rb +23 -0
- data/spec/fixtures/add_table_with_enum_set.schema +5 -0
- data/spec/fixtures/change_table_comment_to_paper.schema +2 -0
- data/spec/fixtures/execute_raw_sql.schema +28 -0
- data/spec/fixtures/postgres/add_columns_to_paper.schema +29 -0
- data/spec/fixtures/postgres/add_table.schema +32 -0
- data/spec/fixtures/postgres/change_comment_columns_to_paper.schema +28 -0
- data/spec/fixtures/postgres/change_table_comment_to_paper.schema +28 -0
- data/spec/fixtures/postgres/drop_foreign_key.schema +25 -0
- data/spec/fixtures/postgres/drop_table.schema +19 -0
- data/spec/fixtures/postgres/remove_columns_to_paper.schema +27 -0
- data/spec/fixtures/postgres_test_db.sql +41 -0
- data/spec/fixtures/sqlite/add_columns_to_paper.schema +29 -0
- data/spec/fixtures/sqlite/add_table.schema +32 -0
- data/spec/fixtures/sqlite/change_columns_to_paper.schema +28 -0
- data/spec/fixtures/sqlite/drop_foreign_key.schema +25 -0
- data/spec/fixtures/sqlite/drop_table.schema +19 -0
- data/spec/fixtures/sqlite/remove_columns_to_paper.schema +27 -0
- data/spec/fixtures/sqlite_test_db.sql +31 -0
- data/spec/fixtures/test_db.sql +10 -0
- data/spec/integrations/command_diff.rb +35 -0
- data/spec/integrations/command_dryrun.rb +37 -2
- data/spec/integrations/command_export.rb +28 -0
- data/spec/postgres_integrations/command_dryrun.rb +79 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/sqlite_integrations/command_dryrun.rb +70 -0
- 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
|
data/lib/convergence/version.rb
CHANGED
|
@@ -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
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
144
|
-
|
|
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:
|
|
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
|
|
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:
|
|
263
|
+
t.varchar('name', limit: 200, null: false)
|
|
190
264
|
end
|
|
191
265
|
end
|
|
192
266
|
|
|
193
|
-
it
|
|
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:
|
|
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
|
|
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(:
|
|
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:
|
|
293
|
+
t.varchar('name', limit: 200, null: false)
|
|
221
294
|
|
|
222
|
-
t.
|
|
295
|
+
t.foreign_key('id', reference: 'ref_tables', reference_column: 'ref_id')
|
|
223
296
|
end
|
|
224
297
|
end
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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:
|
|
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
|
|
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
|
|
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)
|
|
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']
|