convergence 1.0.6 → 1.2.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 +57 -0
- data/CONTRIBUTING.md +80 -0
- data/Gemfile.lock +25 -9
- data/README.md +160 -1
- data/Rakefile +45 -2
- data/convergence.gemspec +5 -1
- data/lib/convergence/cli.rb +10 -2
- data/lib/convergence/column.rb +3 -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 +46 -3
- 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 +32 -3
- data/lib/convergence/sql_generator/postgres_generator.rb +270 -0
- data/lib/convergence/sql_generator/sqlite_generator.rb +192 -0
- data/lib/convergence/table.rb +3 -2
- 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 +291 -44
- 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 +46 -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/rename_column_on_paper.schema +28 -0
- data/spec/fixtures/postgres/rename_table.schema +28 -0
- data/spec/fixtures/postgres_test_db.sql +41 -0
- data/spec/fixtures/rename_column_to_author.schema +26 -0
- data/spec/fixtures/rename_table.schema +26 -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/rename_column_on_paper.schema +28 -0
- data/spec/fixtures/sqlite/rename_table.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 +59 -2
- data/spec/integrations/command_export.rb +28 -0
- data/spec/postgres_integrations/command_dryrun.rb +101 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/sqlite_integrations/command_dryrun.rb +92 -0
- metadata +135 -7
|
@@ -8,40 +8,69 @@ require 'convergence/diff'
|
|
|
8
8
|
class Convergence::Command::Apply < Convergence::Command
|
|
9
9
|
def execute
|
|
10
10
|
current_dir_path = Pathname.new(@opts[:input]).realpath.dirname
|
|
11
|
-
|
|
11
|
+
dsl = Convergence::DSL.parse_dsl(File.open(@opts[:input]).read, current_dir_path)
|
|
12
12
|
current_tables = dumper.dump
|
|
13
|
-
execute_sql(
|
|
13
|
+
execute_sql(dsl.tables, current_tables, dsl.raw_sqls)
|
|
14
14
|
end
|
|
15
15
|
|
|
16
|
-
def generate_sql(input_tables, current_tables)
|
|
16
|
+
def generate_sql(input_tables, current_tables, raw_sqls = [])
|
|
17
17
|
current_tables_with_full_option =
|
|
18
18
|
Convergence::DefaultParameter.append_database_default_parameter(current_tables, database_adapter)
|
|
19
19
|
input_tables_with_full_option =
|
|
20
20
|
Convergence::DefaultParameter.append_database_default_parameter(input_tables, database_adapter)
|
|
21
|
-
delta = Convergence::Diff
|
|
22
|
-
|
|
21
|
+
delta = Convergence::Diff
|
|
22
|
+
.new(ignore_auto_increment: @opts[:ignore_auto_increment])
|
|
23
|
+
.diff(current_tables_with_full_option, input_tables_with_full_option)
|
|
24
|
+
sql = sql_generator.generate(
|
|
25
|
+
input_tables_with_full_option,
|
|
26
|
+
delta,
|
|
27
|
+
current_tables_with_full_option,
|
|
28
|
+
safe_migration: @opts[:safe_migration]
|
|
29
|
+
)
|
|
30
|
+
append_raw_sqls(sql, raw_sqls)
|
|
23
31
|
end
|
|
24
32
|
|
|
25
33
|
private
|
|
26
34
|
|
|
35
|
+
def append_raw_sqls(sql, raw_sqls)
|
|
36
|
+
return sql if raw_sqls.empty?
|
|
37
|
+
raw_sql_block = raw_sqls.map { |q| q.strip.end_with?(';') ? q.strip : "#{q.strip};" }.join("\n")
|
|
38
|
+
[sql, raw_sql_block].reject(&:empty?).join("\n")
|
|
39
|
+
end
|
|
40
|
+
|
|
27
41
|
def sql_generator
|
|
28
42
|
@sql_generator ||= case database_adapter
|
|
29
43
|
when 'mysql', 'mysql2'
|
|
30
44
|
require 'convergence/sql_generator/mysql_generator'
|
|
31
45
|
SQLGenerator::MysqlGenerator.new
|
|
46
|
+
when 'postgresql', 'postgres', 'pg'
|
|
47
|
+
require 'convergence/sql_generator/postgres_generator'
|
|
48
|
+
SQLGenerator::PostgresGenerator.new
|
|
49
|
+
when 'sqlite3', 'sqlite'
|
|
50
|
+
require 'convergence/sql_generator/sqlite_generator'
|
|
51
|
+
SQLGenerator::SqliteGenerator.new
|
|
32
52
|
else
|
|
33
53
|
fail NotImplementedError.new('unknown database adapter')
|
|
34
54
|
end
|
|
35
55
|
end
|
|
36
56
|
|
|
37
|
-
def
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
57
|
+
def wrap_with_constraint_pragma(sql)
|
|
58
|
+
case database_adapter
|
|
59
|
+
when 'mysql', 'mysql2'
|
|
60
|
+
<<-SQL
|
|
41
61
|
SET FOREIGN_KEY_CHECKS=0;
|
|
42
62
|
#{sql}
|
|
43
63
|
SET FOREIGN_KEY_CHECKS=1;
|
|
44
64
|
SQL
|
|
65
|
+
else
|
|
66
|
+
sql
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def execute_sql(input_tables, current_tables, raw_sqls = [])
|
|
71
|
+
sql = generate_sql(input_tables, current_tables, raw_sqls)
|
|
72
|
+
unless sql.strip.empty?
|
|
73
|
+
sql = wrap_with_constraint_pragma(sql)
|
|
45
74
|
end
|
|
46
75
|
sql.split(';').each do |q2|
|
|
47
76
|
q = q2.strip
|
|
@@ -8,11 +8,11 @@ require 'convergence/pretty_diff'
|
|
|
8
8
|
class Convergence::Command::Dryrun < Convergence::Command
|
|
9
9
|
def execute
|
|
10
10
|
current_dir_path = Pathname.new(@opts[:input]).realpath.dirname
|
|
11
|
-
|
|
11
|
+
dsl = Convergence::DSL.parse_dsl(File.open(@opts[:input]).read, current_dir_path)
|
|
12
12
|
current_tables = dumper.dump
|
|
13
13
|
# -- maybe it's redundant output
|
|
14
|
-
# output_diff(
|
|
15
|
-
output_sql(
|
|
14
|
+
# output_diff(dsl.tables, current_tables)
|
|
15
|
+
output_sql(dsl.tables, current_tables, dsl.raw_sqls)
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
private
|
|
@@ -30,10 +30,10 @@ class Convergence::Command::Dryrun < Convergence::Command
|
|
|
30
30
|
msg
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
def output_sql(input_tables, current_tables)
|
|
33
|
+
def output_sql(input_tables, current_tables, raw_sqls = [])
|
|
34
34
|
msg = Convergence::Command::Apply
|
|
35
35
|
.new(@opts, config: @config)
|
|
36
|
-
.generate_sql(input_tables, current_tables)
|
|
36
|
+
.generate_sql(input_tables, current_tables, raw_sqls)
|
|
37
37
|
.split("\n")
|
|
38
38
|
.map { |v| '# ' + v }
|
|
39
39
|
.join("\n")
|
|
@@ -5,8 +5,26 @@ require 'convergence/default_parameter'
|
|
|
5
5
|
class Convergence::Command::Export < Convergence::Command
|
|
6
6
|
def execute
|
|
7
7
|
tables = Convergence::DefaultParameter.remove_database_default_parameter(dumper.dump, database_adapter)
|
|
8
|
-
msg =
|
|
8
|
+
msg = if @opts[:dump_rails_migration]
|
|
9
|
+
dump_rails_migration(tables)
|
|
10
|
+
else
|
|
11
|
+
Convergence::Dumper.new.dump_dsl(tables)
|
|
12
|
+
end
|
|
9
13
|
logger.output(msg)
|
|
10
14
|
msg
|
|
11
15
|
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def dump_rails_migration(tables)
|
|
20
|
+
filename = @opts[:filename] || 'convergence_migration'
|
|
21
|
+
class_name = camelize(filename)
|
|
22
|
+
migration_filename = "#{Time.now.strftime('%Y%m%d%H%M%S')}_#{filename}.rb"
|
|
23
|
+
body = Convergence::Dumper.new.dump_rails_migration(tables, class_name)
|
|
24
|
+
"# Filename: #{migration_filename}\n#{body}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def camelize(name)
|
|
28
|
+
name.to_s.split(/[_-]/).reject(&:empty?).map { |part| part[0].upcase + part[1..].to_s }.join
|
|
29
|
+
end
|
|
12
30
|
end
|
data/lib/convergence/command.rb
CHANGED
|
@@ -27,6 +27,12 @@ class Convergence::Command
|
|
|
27
27
|
when 'mysql', 'mysql2'
|
|
28
28
|
require 'convergence/dumper/mysql_schema_dumper'
|
|
29
29
|
Convergence::Dumper::MysqlSchemaDumper.new(connector)
|
|
30
|
+
when 'postgresql', 'postgres', 'pg'
|
|
31
|
+
require 'convergence/dumper/postgres_schema_dumper'
|
|
32
|
+
Convergence::Dumper::PostgresSchemaDumper.new(connector)
|
|
33
|
+
when 'sqlite3', 'sqlite'
|
|
34
|
+
require 'convergence/dumper/sqlite_schema_dumper'
|
|
35
|
+
Convergence::Dumper::SqliteSchemaDumper.new(connector)
|
|
30
36
|
else
|
|
31
37
|
fail NotImplementedError.new('unknown database adapter')
|
|
32
38
|
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
require 'pg'
|
|
2
|
+
require 'convergence/database_connector'
|
|
3
|
+
|
|
4
|
+
class Convergence::DatabaseConnector::PostgresConnector
|
|
5
|
+
attr_reader :config
|
|
6
|
+
|
|
7
|
+
def initialize(config)
|
|
8
|
+
@config = config
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def client(database_name = @config.database)
|
|
12
|
+
@clients ||= {}
|
|
13
|
+
@clients[database_name] ||= PG::Connection.new(
|
|
14
|
+
host: @config.host,
|
|
15
|
+
port: @config.port,
|
|
16
|
+
user: @config.username,
|
|
17
|
+
password: @config.password,
|
|
18
|
+
dbname: database_name
|
|
19
|
+
)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def schema_client
|
|
23
|
+
client
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require 'sqlite3'
|
|
2
|
+
require 'convergence/database_connector'
|
|
3
|
+
|
|
4
|
+
class Convergence::DatabaseConnector::SqliteConnector
|
|
5
|
+
attr_reader :config
|
|
6
|
+
|
|
7
|
+
def initialize(config)
|
|
8
|
+
@config = config
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def client(database_name = @config.database)
|
|
12
|
+
@clients ||= {}
|
|
13
|
+
@clients[database_name] ||= SQLite3::Database.new(database_name).tap do |db|
|
|
14
|
+
db.results_as_hash = true
|
|
15
|
+
db.execute('PRAGMA foreign_keys = ON')
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def schema_client
|
|
20
|
+
client
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -7,6 +7,12 @@ class Convergence::DatabaseConnector
|
|
|
7
7
|
when 'mysql', 'mysql2'
|
|
8
8
|
require 'convergence/database_connector/mysql_connector'
|
|
9
9
|
Convergence::DatabaseConnector::MysqlConnector.new(config)
|
|
10
|
+
when 'postgresql', 'postgres', 'pg'
|
|
11
|
+
require 'convergence/database_connector/postgres_connector'
|
|
12
|
+
Convergence::DatabaseConnector::PostgresConnector.new(config)
|
|
13
|
+
when 'sqlite3', 'sqlite'
|
|
14
|
+
require 'convergence/database_connector/sqlite_connector'
|
|
15
|
+
Convergence::DatabaseConnector::SqliteConnector.new(config)
|
|
10
16
|
else
|
|
11
17
|
fail NotImplementedError.new("#{config.adapter} not supported yet")
|
|
12
18
|
end
|
|
@@ -51,7 +51,7 @@ class Convergence::DefaultParameter::MysqlDefaultParameter
|
|
|
51
51
|
DEFAULT_COLUMN_PARAMETERS = {
|
|
52
52
|
null: false
|
|
53
53
|
}
|
|
54
|
-
TEXT_TYPE = [:varchar, :char, :tiny_text, :text, :mediumtext, :longtext]
|
|
54
|
+
TEXT_TYPE = [:varchar, :char, :tiny_text, :text, :mediumtext, :longtext, :enum, :set]
|
|
55
55
|
DEFAULT_COLUMN_TYPE_PARAMETERS = {
|
|
56
56
|
tinyint: {
|
|
57
57
|
limit: 4
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require 'convergence/default_parameter'
|
|
2
|
+
|
|
3
|
+
class Convergence::DefaultParameter::PostgresDefaultParameter
|
|
4
|
+
DEFAULT_COLUMN_PARAMETERS = {
|
|
5
|
+
null: false
|
|
6
|
+
}
|
|
7
|
+
DEFAULT_INDEX_PARAMETERS = { type: 'btree', unique: false }
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def remove_default_parameter(table)
|
|
13
|
+
remove_column_default_parameter(table)
|
|
14
|
+
remove_index_default_parameter(table)
|
|
15
|
+
table
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def append_default_parameter(table)
|
|
19
|
+
append_column_default_parameter(table)
|
|
20
|
+
append_index_default_parameter(table)
|
|
21
|
+
table
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def remove_column_default_parameter(table)
|
|
27
|
+
table.columns.each do |_column_name, column|
|
|
28
|
+
DEFAULT_COLUMN_PARAMETERS.each do |k, v|
|
|
29
|
+
if !column.options[k].nil? && column.options[k].to_s.downcase == v.to_s.downcase
|
|
30
|
+
column.options.delete(k)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def remove_index_default_parameter(table)
|
|
37
|
+
table.indexes.each do |_, va|
|
|
38
|
+
va.options.each do |k, v|
|
|
39
|
+
if !DEFAULT_INDEX_PARAMETERS[k].nil? && DEFAULT_INDEX_PARAMETERS[k].to_s.downcase == v.to_s.downcase
|
|
40
|
+
va.options.delete(k)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def append_column_default_parameter(table)
|
|
47
|
+
table.columns.each do |_column_name, column|
|
|
48
|
+
column.options = DEFAULT_COLUMN_PARAMETERS.merge(column.options)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def append_index_default_parameter(table)
|
|
53
|
+
table.indexes.each do |_column_name, column|
|
|
54
|
+
column.options = DEFAULT_INDEX_PARAMETERS.merge(column.options)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require 'convergence/default_parameter'
|
|
2
|
+
|
|
3
|
+
class Convergence::DefaultParameter::SqliteDefaultParameter
|
|
4
|
+
DEFAULT_COLUMN_PARAMETERS = {
|
|
5
|
+
null: false
|
|
6
|
+
}
|
|
7
|
+
DEFAULT_INDEX_PARAMETERS = { unique: false }
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def remove_default_parameter(table)
|
|
13
|
+
remove_column_default_parameter(table)
|
|
14
|
+
remove_index_default_parameter(table)
|
|
15
|
+
table
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def append_default_parameter(table)
|
|
19
|
+
append_column_default_parameter(table)
|
|
20
|
+
append_index_default_parameter(table)
|
|
21
|
+
table
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def remove_column_default_parameter(table)
|
|
27
|
+
table.columns.each do |_column_name, column|
|
|
28
|
+
DEFAULT_COLUMN_PARAMETERS.each do |k, v|
|
|
29
|
+
if !column.options[k].nil? && column.options[k].to_s.downcase == v.to_s.downcase
|
|
30
|
+
column.options.delete(k)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def remove_index_default_parameter(table)
|
|
37
|
+
table.indexes.each do |_, va|
|
|
38
|
+
va.options.each do |k, v|
|
|
39
|
+
if !DEFAULT_INDEX_PARAMETERS[k].nil? && DEFAULT_INDEX_PARAMETERS[k].to_s.downcase == v.to_s.downcase
|
|
40
|
+
va.options.delete(k)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def append_column_default_parameter(table)
|
|
47
|
+
table.columns.each do |_column_name, column|
|
|
48
|
+
column.options = DEFAULT_COLUMN_PARAMETERS.merge(column.options)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def append_index_default_parameter(table)
|
|
53
|
+
table.indexes.each do |_column_name, column|
|
|
54
|
+
column.options = DEFAULT_INDEX_PARAMETERS.merge(column.options)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -4,6 +4,12 @@ class Convergence::DefaultParameter
|
|
|
4
4
|
when 'mysql', 'mysql2'
|
|
5
5
|
require 'convergence/default_parameter/mysql_default_parameter'
|
|
6
6
|
@parameter_klass = Convergence::DefaultParameter::MysqlDefaultParameter.new
|
|
7
|
+
when 'postgresql', 'postgres', 'pg'
|
|
8
|
+
require 'convergence/default_parameter/postgres_default_parameter'
|
|
9
|
+
@parameter_klass = Convergence::DefaultParameter::PostgresDefaultParameter.new
|
|
10
|
+
when 'sqlite3', 'sqlite'
|
|
11
|
+
require 'convergence/default_parameter/sqlite_default_parameter'
|
|
12
|
+
@parameter_klass = Convergence::DefaultParameter::SqliteDefaultParameter.new
|
|
7
13
|
else
|
|
8
14
|
fail NotImplementedError.new("unknown adapter #{config.adapter}.")
|
|
9
15
|
end
|
data/lib/convergence/diff.rb
CHANGED
|
@@ -2,11 +2,18 @@ require 'diff/lcs'
|
|
|
2
2
|
|
|
3
3
|
class Convergence::Diff
|
|
4
4
|
CASE_SENSITIVE_TABLE_OPTIONS = %i(comment)
|
|
5
|
-
CASE_SENSITIVE_COLUMNS = %i(default comment)
|
|
5
|
+
CASE_SENSITIVE_COLUMNS = %i(default comment values)
|
|
6
|
+
|
|
7
|
+
def initialize(ignore_auto_increment: false)
|
|
8
|
+
@ignore_auto_increment = ignore_auto_increment
|
|
9
|
+
end
|
|
6
10
|
|
|
7
11
|
def diff(from_database, to_database)
|
|
8
12
|
delta = {}
|
|
9
13
|
from_database = {} if from_database.nil?
|
|
14
|
+
delta[:rename_table] = scan_rename_table(from_database, to_database)
|
|
15
|
+
from_database = from_database.reject { |name, _| delta[:rename_table].key?(name) }
|
|
16
|
+
to_database = to_database.reject { |name, _| delta[:rename_table].value?(name) }
|
|
10
17
|
delta[:add_table] = scan_add_table(from_database, to_database)
|
|
11
18
|
delta[:remove_table] = scan_remove_table(from_database, to_database)
|
|
12
19
|
change_table = scan_change_table(from_database, to_database)
|
|
@@ -20,8 +27,11 @@ class Convergence::Diff
|
|
|
20
27
|
from = from_table.dup
|
|
21
28
|
to = to_table.dup
|
|
22
29
|
delta = {}
|
|
30
|
+
delta[:rename_column] = scan_rename_column(from, to)
|
|
31
|
+
from.columns = from.columns.reject { |name, _| delta[:rename_column].key?(name) }
|
|
32
|
+
to.columns = to.columns.reject { |name, _| delta[:rename_column].value?(name) }
|
|
23
33
|
delta[:remove_column] = scan_remove_column(from, to)
|
|
24
|
-
return delta if removed_all_columns?(
|
|
34
|
+
return delta if removed_all_columns?(from_table, delta)
|
|
25
35
|
delta[:add_column] = scan_add_column(from, to)
|
|
26
36
|
delta[:change_column] = scan_change_column(from, to)
|
|
27
37
|
scan_change_order_column(from, to, delta)
|
|
@@ -35,6 +45,36 @@ class Convergence::Diff
|
|
|
35
45
|
|
|
36
46
|
private
|
|
37
47
|
|
|
48
|
+
def scan_rename_table(from, to)
|
|
49
|
+
renames = {}
|
|
50
|
+
to.each do |new_name, to_table|
|
|
51
|
+
old_name = to_table.renamed_from
|
|
52
|
+
next if old_name.nil?
|
|
53
|
+
next unless from.key?(old_name)
|
|
54
|
+
next if from.key?(new_name)
|
|
55
|
+
if renames.key?(old_name)
|
|
56
|
+
fail ArgumentError.new("renamed_from '#{old_name}' is specified for multiple tables")
|
|
57
|
+
end
|
|
58
|
+
renames[old_name] = new_name
|
|
59
|
+
end
|
|
60
|
+
renames
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def scan_rename_column(from, to)
|
|
64
|
+
renames = {}
|
|
65
|
+
to.columns.each do |new_name, to_column|
|
|
66
|
+
old_name = to_column.renamed_from
|
|
67
|
+
next if old_name.nil?
|
|
68
|
+
next unless from.columns.key?(old_name)
|
|
69
|
+
next if from.columns.key?(new_name)
|
|
70
|
+
if renames.key?(old_name)
|
|
71
|
+
fail ArgumentError.new("#{to.table_name}: renamed_from '#{old_name}' is specified for multiple columns")
|
|
72
|
+
end
|
|
73
|
+
renames[old_name] = new_name
|
|
74
|
+
end
|
|
75
|
+
renames
|
|
76
|
+
end
|
|
77
|
+
|
|
38
78
|
def scan_add_table(from, to)
|
|
39
79
|
to.reject { |table_name, _| from.map { |k, _| k }.include?(table_name) }
|
|
40
80
|
end
|
|
@@ -76,14 +116,16 @@ class Convergence::Diff
|
|
|
76
116
|
.map do |column_name, from_column|
|
|
77
117
|
to_column = to.columns[column_name]
|
|
78
118
|
if to_column
|
|
79
|
-
to_column_option_with_type = (from_column.options.map { |k, _v|
|
|
119
|
+
to_column_option_with_type = (from_column.options.map { |k, _v| [k, nil] }.to_h)
|
|
80
120
|
.merge(to_column.options)
|
|
81
121
|
.merge(type: to_column.type)
|
|
122
|
+
.tap { |opt| opt[:default] = opt[:default].call if opt[:default].is_a?(Proc) }
|
|
82
123
|
.map { |k, v| [k, case_sensitive_column?(k) ? v&.to_s : v&.to_s&.downcase] }
|
|
83
124
|
.to_a
|
|
84
125
|
from_column_option_with_type = from_column
|
|
85
126
|
.options
|
|
86
127
|
.merge(type: from_column.type)
|
|
128
|
+
.tap { |opt| opt[:default] = opt[:default].call if opt[:default].is_a?(Proc) }
|
|
87
129
|
.map { |k, v| [k, case_sensitive_column?(k) ? v&.to_s : v&.to_s&.downcase] }
|
|
88
130
|
.to_a
|
|
89
131
|
{ column_name => Hash[(to_column_option_with_type - from_column_option_with_type)] }
|
|
@@ -175,6 +217,7 @@ class Convergence::Diff
|
|
|
175
217
|
end
|
|
176
218
|
|
|
177
219
|
def remove_auto_increment_option?(from_value, to_value)
|
|
220
|
+
return true if @ignore_auto_increment
|
|
178
221
|
return true if from_value.nil? || to_value.nil?
|
|
179
222
|
from_value >= to_value
|
|
180
223
|
end
|
data/lib/convergence/dsl.rb
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
require 'convergence/table'
|
|
2
2
|
|
|
3
3
|
class Convergence::DSL
|
|
4
|
-
attr_accessor :tables, :current_dir_path
|
|
4
|
+
attr_accessor :tables, :raw_sqls, :current_dir_path
|
|
5
5
|
|
|
6
6
|
def initialize
|
|
7
7
|
@tables = {}
|
|
8
|
+
@raw_sqls = []
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
def create_table(table_name, options = {}, &block)
|
|
@@ -14,15 +15,29 @@ class Convergence::DSL
|
|
|
14
15
|
table
|
|
15
16
|
end
|
|
16
17
|
|
|
18
|
+
# Execute an arbitrary SQL statement that the DSL has no dedicated syntax for
|
|
19
|
+
# (e.g. triggers, stored procedures, data backfills). Unlike create_table,
|
|
20
|
+
# this is not diffed against the current schema: it runs every time the
|
|
21
|
+
# schema file is applied, so the SQL itself must be idempotent.
|
|
22
|
+
def execute(sql)
|
|
23
|
+
@raw_sqls << sql
|
|
24
|
+
end
|
|
25
|
+
|
|
17
26
|
def include(path)
|
|
18
27
|
next_dir_path = File.dirname("#{@current_dir_path}/#{path}")
|
|
19
|
-
|
|
28
|
+
included = Convergence::DSL.parse_dsl(File.open("#{current_dir_path}/#{path}").read, next_dir_path)
|
|
29
|
+
@tables.merge!(included.tables)
|
|
30
|
+
@raw_sqls.concat(included.raw_sqls)
|
|
20
31
|
end
|
|
21
32
|
|
|
22
33
|
def self.parse(code, current_dir_path)
|
|
34
|
+
parse_dsl(code, current_dir_path).tables
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def self.parse_dsl(code, current_dir_path)
|
|
23
38
|
parser = new
|
|
24
39
|
parser.current_dir_path = current_dir_path
|
|
25
40
|
parser.instance_eval(code)
|
|
26
|
-
parser
|
|
41
|
+
parser
|
|
27
42
|
end
|
|
28
43
|
end
|
|
@@ -110,8 +110,8 @@ class Convergence::Dumper::MysqlSchemaDumper
|
|
|
110
110
|
option.merge!(engine: table_option['ENGINE'])
|
|
111
111
|
row_format = table_option['CREATE_OPTIONS'].scan(/=(.*)/).flatten[0] || table_option['ROW_FORMAT']
|
|
112
112
|
option.merge!(row_format: row_format)
|
|
113
|
-
option.merge!(default_charset: table_option['CHARACTER_SET_NAME'])
|
|
114
|
-
option.merge!(collate: table_option['TABLE_COLLATION'])
|
|
113
|
+
option.merge!(default_charset: normalize_charset(table_option['CHARACTER_SET_NAME']))
|
|
114
|
+
option.merge!(collate: normalize_charset(table_option['TABLE_COLLATION']))
|
|
115
115
|
option.merge!(comment: table_option['TABLE_COMMENT'])
|
|
116
116
|
option.merge!(auto_increment: table_option['AUTO_INCREMENT']) if table_option['AUTO_INCREMENT']
|
|
117
117
|
table.table_options = option
|
|
@@ -125,16 +125,19 @@ class Convergence::Dumper::MysqlSchemaDumper
|
|
|
125
125
|
end
|
|
126
126
|
|
|
127
127
|
def parse_column(column)
|
|
128
|
-
data_type = column['DATA_TYPE']
|
|
128
|
+
data_type = column['DATA_TYPE'].to_sym
|
|
129
129
|
column_name = column['COLUMN_NAME']
|
|
130
130
|
options = { null: column['IS_NULLABLE'] == 'YES' ? true : false }
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
131
|
+
unless column['COLUMN_DEFAULT'].nil?
|
|
132
|
+
options.merge!(default: column_default_expression(data_type, column['COLUMN_DEFAULT']))
|
|
133
|
+
end
|
|
134
|
+
options.merge!(character_set: normalize_charset(column['CHARACTER_SET_NAME'])) unless column['CHARACTER_SET_NAME'].nil?
|
|
135
|
+
options.merge!(collate: normalize_charset(column['COLLATION_NAME'])) unless column['COLLATION_NAME'].nil?
|
|
134
136
|
column_type = column['COLUMN_TYPE']
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
case data_type
|
|
138
|
+
when :enum, :set
|
|
139
|
+
options.merge!(values: parse_enum_or_set_values(column_type))
|
|
140
|
+
when :decimal
|
|
138
141
|
precision, scale = column_type.scan(/\d+/)
|
|
139
142
|
options.merge!(precision: precision, scale: scale)
|
|
140
143
|
else
|
|
@@ -179,4 +182,29 @@ class Convergence::Dumper::MysqlSchemaDumper
|
|
|
179
182
|
end
|
|
180
183
|
end
|
|
181
184
|
end
|
|
185
|
+
|
|
186
|
+
# column_type looks like "enum('a','b','c')" or "set('a','b','c')"
|
|
187
|
+
def parse_enum_or_set_values(column_type)
|
|
188
|
+
column_type[/\A(?:enum|set)\((.*)\)\z/i, 1]
|
|
189
|
+
.to_s
|
|
190
|
+
.scan(/'((?:[^']|'')*)'/)
|
|
191
|
+
.flatten
|
|
192
|
+
.map { |v| v.gsub("''", "'") }
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
# MySQL 8.0.24+ reports the `utf8` charset/collation as `utf8mb3` (e.g. `utf8mb3_general_ci`).
|
|
196
|
+
# Normalize it back to `utf8` so it still matches schemas written with the traditional name.
|
|
197
|
+
def normalize_charset(value)
|
|
198
|
+
return value if value.nil?
|
|
199
|
+
value.sub(/\Autf8mb3/, 'utf8')
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def column_default_expression(data_type, value)
|
|
203
|
+
case [data_type, value]
|
|
204
|
+
when [:datetime, "CURRENT_TIMESTAMP"], [:timestamp, "CURRENT_TIMESTAMP"]
|
|
205
|
+
-> { value }
|
|
206
|
+
else
|
|
207
|
+
value
|
|
208
|
+
end
|
|
209
|
+
end
|
|
182
210
|
end
|