convergence 1.1.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 75c8ae12a9ee346dd376ff2dfe6f03451a96aa4dc58acf414f9c0447c3e79bab
4
- data.tar.gz: 57e2096d724239594acd415001e801f89f22bba706e3382ccca46dd3309cd5b9
3
+ metadata.gz: 633f45bac912daa7950afa5535bfd87d13a7b33b25ff72489fb7fa125d0b6bdd
4
+ data.tar.gz: 392d293827a735cfeb515e878eb6f565d593259d0ff9f36e7aae8c0b4076b1ec
5
5
  SHA512:
6
- metadata.gz: ca8171ecd7a63c898401fb316be440a1cee97b4cd44d52bda5ac449c608dd54432574aa5ce6e2222fe0d65acac0e89d4ec092eecd6e83cb6a3e9874ffb382cbc
7
- data.tar.gz: 3b7ca04775d9e8800a69f6b70034d9d906b8ba679a575603b8d7d5283db2dd031215d50c42e4d16993f2e686c38ba4bb04ffa4d1028eac2f896f52df05c99f31
6
+ metadata.gz: f4255769af38418d8d336b8db4ef7e1ec2844ed3fbcf99cfd4e4bafa055e9765e5d30aaa92d2962ea8152c0b0688821e632ab71e2700158bcbe2f6a212f06270
7
+ data.tar.gz: 481f9bbebb0423275c0695445ab065ca6d54771f7dc25f94b005ac2918a1d7ad94730766a0d8a7c869fb76e49d7b6cc8e8da7cfea177d77c6fd2c049dbf0051b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,14 @@
1
+ ## Convergence 1.2.0 (August 26, 2026) ##
2
+
3
+ * Add `renamed_from:` support to detect table/column renames instead of a
4
+ destructive drop+add, for MySQL, PostgreSQL, and SQLite (PR: #101)
5
+
6
+ *nishio-dens*
7
+
8
+ * Bump rexml to fix CVE-2024-49761 (PR: #103)
9
+
10
+ *nishio-dens*
11
+
1
12
  ## Convergence 1.1.0 (August 25, 2026) ##
2
13
 
3
14
  * Add PostgreSQL support (PR: #93)
data/Gemfile.lock CHANGED
@@ -1,9 +1,11 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- convergence (1.1.0)
4
+ convergence (1.2.0)
5
+ benchmark
5
6
  diff-lcs
6
7
  diffy (>= 3.4.2)
8
+ logger
7
9
  mysql2
8
10
  pg
9
11
  sqlite3
@@ -13,6 +15,7 @@ GEM
13
15
  remote: https://rubygems.org/
14
16
  specs:
15
17
  ast (2.4.1)
18
+ benchmark (0.5.0)
16
19
  bigdecimal (4.1.2)
17
20
  coderay (1.1.3)
18
21
  diff-lcs (1.4.4)
@@ -37,6 +40,7 @@ GEM
37
40
  listen (3.3.3)
38
41
  rb-fsevent (~> 0.10, >= 0.10.3)
39
42
  rb-inotify (~> 0.9, >= 0.9.10)
43
+ logger (1.7.0)
40
44
  lumberjack (1.2.8)
41
45
  method_source (1.1.0)
42
46
  mini_portile2 (2.8.9)
@@ -50,7 +54,6 @@ GEM
50
54
  parser (3.0.0.0)
51
55
  ast (~> 2.4.1)
52
56
  pg (1.6.3)
53
- pg (1.6.3-x86_64-linux)
54
57
  pry (0.16.0)
55
58
  coderay (~> 1.1)
56
59
  method_source (~> 1.0)
@@ -63,7 +66,7 @@ GEM
63
66
  regexp_parser (2.0.3)
64
67
  reline (0.7.0)
65
68
  io-console (~> 0.5)
66
- rexml (3.2.5)
69
+ rexml (3.4.4)
67
70
  rspec (3.10.0)
68
71
  rspec-core (~> 3.10.0)
69
72
  rspec-expectations (~> 3.10.0)
@@ -92,13 +95,11 @@ GEM
92
95
  shellany (0.0.1)
93
96
  sqlite3 (2.8.1)
94
97
  mini_portile2 (~> 2.8.0)
95
- sqlite3 (2.8.1-x86_64-linux-gnu)
96
98
  thor (1.0.1)
97
99
  unicode-display_width (1.7.0)
98
100
 
99
101
  PLATFORMS
100
102
  ruby
101
- x86_64-linux
102
103
 
103
104
  DEPENDENCIES
104
105
  convergence!
@@ -110,4 +111,4 @@ DEPENDENCIES
110
111
  rubocop
111
112
 
112
113
  BUNDLED WITH
113
- 4.0.19
114
+ 4.0.19
data/README.md CHANGED
@@ -191,7 +191,9 @@ create/drop indexes on an existing table. Changing a column's type/null/default,
191
191
  foreign key or primary key on an existing table, all require SQLite's own "rebuild the table" procedure
192
192
  (create a new table, copy the data over, drop the old one, rename it), which this adapter does not implement
193
193
  yet -- it raises `NotImplementedError` instead of generating SQL that SQLite would reject. Creating a brand new
194
- table with any of the above (including foreign keys) works fine.
194
+ table with any of the above (including foreign keys) works fine. Renaming a table/column via `renamed_from:`
195
+ (see [Rename tables and columns](#rename-tables-and-columns)) is an exception to this limitation -- SQLite has
196
+ natively supported `RENAME TABLE`/`RENAME COLUMN` since 3.25.0, so it doesn't need the rebuild procedure.
195
197
 
196
198
  Like PostgreSQL, MySQL-only table options (`engine`, `row_format`, `default_charset`/`character_set`, `collate`,
197
199
  table/column `comment`) have no SQLite equivalent and are ignored. `extra: 'auto_increment'` maps to SQLite's
@@ -340,6 +342,33 @@ $ convergence apply example.schema -c database.yml --safe-migration
340
342
 
341
343
  This also applies to `--dry-run` and `--rollback-dry-run`.
342
344
 
345
+ ### Rename tables and columns
346
+
347
+ Without `renamed_from:`, convergence detects a renamed table/column as a drop followed by an add -- which loses
348
+ the data in that table/column. Add `renamed_from:` to the new name so convergence generates `RENAME TABLE`/`RENAME
349
+ COLUMN` instead:
350
+
351
+ ```ruby
352
+ create_table "user_accounts", renamed_from: "users" do |t|
353
+ t.int "id", primary_key: true, extra: "auto_increment"
354
+ t.varchar "full_name", limit: 200, renamed_from: "name"
355
+ end
356
+ ```
357
+
358
+ A few things to know:
359
+
360
+ * When a table or column has `renamed_from:`, **only the rename is applied on that pass** -- any other change to
361
+ the same table/column (a type change, a new `null`/`default`, ...) is ignored until you run `apply` again. Run
362
+ `apply` a second time (with `renamed_from:` still in place, or removed) to pick up the rest.
363
+ * `renamed_from:` is safe to leave in the schema file after the rename has been applied -- convergence detects
364
+ that the target name already exists and treats it as a no-op.
365
+ * Swapping two names (A ↔ B) or chaining renames (A → B → C) in a single `apply` is not supported. Use an
366
+ intermediate temporary name and apply in multiple steps instead.
367
+ * MySQL's `RENAME COLUMN` requires MySQL 8.0+; there's no fallback for 5.7.
368
+ * `convergence diff`'s pretty-printed output does not yet understand renames -- it will describe a rename as "no
369
+ change" for the renamed table/column. This is a known limitation, unrelated to the SQL that `apply`/`dry-run`
370
+ actually generate.
371
+
343
372
  ### Include Other Schema files
344
373
 
345
374
  ```
data/convergence.gemspec CHANGED
@@ -24,6 +24,8 @@ Gem::Specification.new do |spec|
24
24
  spec.add_dependency 'diff-lcs'
25
25
  spec.add_dependency 'diffy', '>= 3.4.2'
26
26
  spec.add_dependency 'thor'
27
+ spec.add_dependency 'logger'
28
+ spec.add_dependency 'benchmark'
27
29
 
28
30
  spec.required_ruby_version = ">= 2.4.0"
29
31
 
@@ -1,5 +1,5 @@
1
1
  class Convergence::Column
2
- attr_accessor :type, :column_name, :options
2
+ attr_accessor :type, :column_name, :options, :renamed_from
3
3
 
4
4
  COLUMN_TYPE = %i(
5
5
  tinyint
@@ -38,6 +38,7 @@ class Convergence::Column
38
38
  def initialize(type, column_name, options = {})
39
39
  @type = type
40
40
  @column_name = column_name
41
- @options = options
41
+ @renamed_from = options[:renamed_from]&.to_s
42
+ @options = options.reject { |k| k == :renamed_from }
42
43
  end
43
44
  end
@@ -11,6 +11,9 @@ class Convergence::Diff
11
11
  def diff(from_database, to_database)
12
12
  delta = {}
13
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) }
14
17
  delta[:add_table] = scan_add_table(from_database, to_database)
15
18
  delta[:remove_table] = scan_remove_table(from_database, to_database)
16
19
  change_table = scan_change_table(from_database, to_database)
@@ -24,8 +27,11 @@ class Convergence::Diff
24
27
  from = from_table.dup
25
28
  to = to_table.dup
26
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) }
27
33
  delta[:remove_column] = scan_remove_column(from, to)
28
- return delta if removed_all_columns?(from, delta)
34
+ return delta if removed_all_columns?(from_table, delta)
29
35
  delta[:add_column] = scan_add_column(from, to)
30
36
  delta[:change_column] = scan_change_column(from, to)
31
37
  scan_change_order_column(from, to, delta)
@@ -39,6 +45,36 @@ class Convergence::Diff
39
45
 
40
46
  private
41
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
+
42
78
  def scan_add_table(from, to)
43
79
  to.reject { |table_name, _| from.map { |k, _| k }.include?(table_name) }
44
80
  end
@@ -16,6 +16,7 @@ class SQLGenerator::MysqlGenerator < SQLGenerator
16
16
  def generate(to_table, delta, original_table, safe_migration: false)
17
17
  @original_table = original_table
18
18
  sqls = []
19
+ sqls << rename_table_sqls(delta)
19
20
  sqls << change_table_sql(to_table, delta)
20
21
  sqls << (safe_migration ? [] : drop_table_sqls(delta))
21
22
  sqls << create_table_sqls(delta)
@@ -25,6 +26,12 @@ class SQLGenerator::MysqlGenerator < SQLGenerator
25
26
 
26
27
  private
27
28
 
29
+ def rename_table_sqls(delta)
30
+ results = delta[:rename_table].map { |old_name, new_name| %(ALTER TABLE `#{old_name}` RENAME TO `#{new_name}`;) }
31
+ results << '' unless results.empty?
32
+ results
33
+ end
34
+
28
35
  # FIXME: multiple pk change not supported yet
29
36
  def change_table_sql(to_table, delta)
30
37
  change_table = delta[:change_table]
@@ -36,6 +43,9 @@ class SQLGenerator::MysqlGenerator < SQLGenerator
36
43
  table_delta[:remove_index].each do |index_name, _index|
37
44
  results << alter_remove_index_sql(table_name, index_name)
38
45
  end
46
+ table_delta[:rename_column].each do |old_name, new_name|
47
+ results << alter_rename_column_sql(table_name, old_name, new_name)
48
+ end
39
49
  unless table_delta[:remove_column].empty?
40
50
  results << alter_remove_columns_sql(table_name, table_delta[:remove_column].values)
41
51
  end
@@ -73,6 +83,10 @@ class SQLGenerator::MysqlGenerator < SQLGenerator
73
83
  sql
74
84
  end
75
85
 
86
+ def alter_rename_column_sql(table_name, old_name, new_name)
87
+ %(ALTER TABLE `#{table_name}` RENAME COLUMN `#{old_name}` TO `#{new_name}`;)
88
+ end
89
+
76
90
  def alter_change_column_sql(table_name, column_name, change_column_option, to_table)
77
91
  column = to_table[table_name].columns[column_name]
78
92
  column.options.merge!(after: change_column_option[:after]) unless change_column_option[:after].nil?
@@ -37,6 +37,7 @@ class SQLGenerator::PostgresGenerator < SQLGenerator
37
37
  def generate(to_table, delta, original_table, safe_migration: false)
38
38
  @original_table = original_table
39
39
  sqls = []
40
+ sqls << rename_table_sqls(delta)
40
41
  sqls << change_table_sql(to_table, delta)
41
42
  sqls << (safe_migration ? [] : drop_table_sqls(delta))
42
43
  sqls << create_table_sqls(delta)
@@ -46,6 +47,12 @@ class SQLGenerator::PostgresGenerator < SQLGenerator
46
47
 
47
48
  private
48
49
 
50
+ def rename_table_sqls(delta)
51
+ results = delta[:rename_table].map { |old_name, new_name| %(ALTER TABLE "#{old_name}" RENAME TO "#{new_name}";) }
52
+ results << '' unless results.empty?
53
+ results
54
+ end
55
+
49
56
  # FIXME: multiple pk change not supported yet
50
57
  def change_table_sql(to_table, delta)
51
58
  change_table = delta[:change_table]
@@ -57,6 +64,9 @@ class SQLGenerator::PostgresGenerator < SQLGenerator
57
64
  table_delta[:remove_index].each do |index_name, _index|
58
65
  results << alter_remove_index_sql(index_name)
59
66
  end
67
+ table_delta[:rename_column].each do |old_name, new_name|
68
+ results << alter_rename_column_sql(table_name, old_name, new_name)
69
+ end
60
70
  unless table_delta[:remove_column].empty?
61
71
  results << alter_remove_columns_sql(table_name, table_delta[:remove_column].values)
62
72
  end
@@ -90,6 +100,10 @@ class SQLGenerator::PostgresGenerator < SQLGenerator
90
100
  columns.map { |column| %(ALTER TABLE "#{table_name}" DROP COLUMN "#{column.column_name}";) }.join("\n")
91
101
  end
92
102
 
103
+ def alter_rename_column_sql(table_name, old_name, new_name)
104
+ %(ALTER TABLE "#{table_name}" RENAME COLUMN "#{old_name}" TO "#{new_name}";)
105
+ end
106
+
93
107
  def alter_change_column_sql(table_name, column_name, change_column_option, to_table)
94
108
  column = to_table[table_name].columns[column_name]
95
109
  column.options.merge!(after: change_column_option[:after]) unless change_column_option[:after].nil?
@@ -15,6 +15,7 @@ class SQLGenerator::SqliteGenerator < SQLGenerator
15
15
  def generate(to_table, delta, original_table, safe_migration: false)
16
16
  @original_table = original_table
17
17
  sqls = []
18
+ sqls << rename_table_sqls(delta)
18
19
  sqls << change_table_sql(to_table, delta)
19
20
  sqls << (safe_migration ? [] : drop_table_sqls(delta))
20
21
  sqls << create_table_sqls(delta)
@@ -24,6 +25,12 @@ class SQLGenerator::SqliteGenerator < SQLGenerator
24
25
 
25
26
  private
26
27
 
28
+ def rename_table_sqls(delta)
29
+ results = delta[:rename_table].map { |old_name, new_name| %(ALTER TABLE "#{old_name}" RENAME TO "#{new_name}";) }
30
+ results << '' unless results.empty?
31
+ results
32
+ end
33
+
27
34
  def change_table_sql(to_table, delta)
28
35
  change_table = delta[:change_table]
29
36
  results = []
@@ -41,6 +48,9 @@ class SQLGenerator::SqliteGenerator < SQLGenerator
41
48
  table_delta[:remove_index].each do |index_name, _index|
42
49
  results << alter_remove_index_sql(index_name)
43
50
  end
51
+ table_delta[:rename_column].each do |old_name, new_name|
52
+ results << alter_rename_column_sql(table_name, old_name, new_name)
53
+ end
44
54
  unless table_delta[:remove_column].empty?
45
55
  results << alter_remove_columns_sql(table_name, table_delta[:remove_column].values)
46
56
  end
@@ -72,6 +82,10 @@ class SQLGenerator::SqliteGenerator < SQLGenerator
72
82
  columns.map { |column| %(ALTER TABLE "#{table_name}" DROP COLUMN "#{column.column_name}";) }.join("\n")
73
83
  end
74
84
 
85
+ def alter_rename_column_sql(table_name, old_name, new_name)
86
+ %(ALTER TABLE "#{table_name}" RENAME COLUMN "#{old_name}" TO "#{new_name}";)
87
+ end
88
+
75
89
  def alter_remove_index_sql(index_name)
76
90
  %(DROP INDEX "#{index_name}";)
77
91
  end
@@ -3,7 +3,7 @@ require 'convergence/index'
3
3
  require 'convergence/foreign_key'
4
4
 
5
5
  class Convergence::Table
6
- attr_accessor :table_name, :table_options, :columns, :indexes, :foreign_keys
6
+ attr_accessor :table_name, :table_options, :columns, :indexes, :foreign_keys, :renamed_from
7
7
 
8
8
  Convergence::Column::COLUMN_TYPE.each do |column_type|
9
9
  define_method "#{column_type}" do |column_name, options = {}|
@@ -46,7 +46,8 @@ class Convergence::Table
46
46
 
47
47
  def initialize(table_name, options = {})
48
48
  @table_name = table_name
49
- @table_options = options.reject { |k| k == :id }
49
+ @renamed_from = options[:renamed_from]&.to_s
50
+ @table_options = options.reject { |k| k == :id || k == :renamed_from }
50
51
  @columns = {}
51
52
  @indexes = {}
52
53
  @foreign_keys = {}
@@ -1,3 +1,3 @@
1
1
  module Convergence
2
- VERSION = '1.1.0'
2
+ VERSION = '1.2.0'
3
3
  end
@@ -64,6 +64,96 @@ describe Convergence::DSL do
64
64
  end
65
65
  end
66
66
 
67
+ context 'when a table is renamed' do
68
+ let(:renamed_table) do
69
+ Convergence::Table.new('simple_table_renamed', renamed_from: 'simple_table').tap do |t|
70
+ t.int('id', primary_key: true)
71
+ end
72
+ end
73
+ let(:from_db) do
74
+ { 'simple_table' => simple_table }
75
+ end
76
+ let(:to_db) do
77
+ { 'simple_table_renamed' => renamed_table }
78
+ end
79
+
80
+ subject { Convergence::Diff.new.diff(from_db, to_db) }
81
+
82
+ it 'should detect it as a rename, not add/remove' do
83
+ expect(subject[:rename_table]).to eq('simple_table' => 'simple_table_renamed')
84
+ expect(subject[:add_table]).to be_empty
85
+ expect(subject[:remove_table]).to be_empty
86
+ expect(subject[:change_table]).to be_empty
87
+ end
88
+ end
89
+
90
+ context 'when renamed_from references a table that does not exist' do
91
+ let(:renamed_table) do
92
+ Convergence::Table.new('simple_table2', renamed_from: 'nonexistent_table').tap do |t|
93
+ t.int('id', primary_key: true)
94
+ end
95
+ end
96
+ let(:from_db) do
97
+ { 'simple_table' => simple_table }
98
+ end
99
+ let(:to_db) do
100
+ { 'simple_table' => simple_table, 'simple_table2' => renamed_table }
101
+ end
102
+
103
+ subject { Convergence::Diff.new.diff(from_db, to_db) }
104
+
105
+ it 'should fall back to a normal add' do
106
+ expect(subject[:rename_table]).to be_empty
107
+ expect(subject[:add_table]['simple_table2']).not_to be_nil
108
+ end
109
+ end
110
+
111
+ context 'when the renamed_from target already exists (idempotent re-apply)' do
112
+ let(:renamed_table) do
113
+ Convergence::Table.new('simple_table2', renamed_from: 'simple_table').tap do |t|
114
+ t.int('id', primary_key: true)
115
+ end
116
+ end
117
+ let(:from_db) do
118
+ { 'simple_table' => simple_table, 'simple_table2' => simple_table2 }
119
+ end
120
+ let(:to_db) do
121
+ { 'simple_table' => simple_table, 'simple_table2' => renamed_table }
122
+ end
123
+
124
+ subject { Convergence::Diff.new.diff(from_db, to_db) }
125
+
126
+ it 'should be a no-op for the rename' do
127
+ expect(subject[:rename_table]).to be_empty
128
+ expect(subject[:add_table]).to be_empty
129
+ expect(subject[:remove_table]).to be_empty
130
+ expect(subject[:change_table]).to be_empty
131
+ end
132
+ end
133
+
134
+ context 'when multiple tables claim the same renamed_from (ambiguous)' do
135
+ let(:renamed_table1) do
136
+ Convergence::Table.new('simple_table_a', renamed_from: 'simple_table').tap do |t|
137
+ t.int('id', primary_key: true)
138
+ end
139
+ end
140
+ let(:renamed_table2) do
141
+ Convergence::Table.new('simple_table_b', renamed_from: 'simple_table').tap do |t|
142
+ t.int('id', primary_key: true)
143
+ end
144
+ end
145
+ let(:from_db) do
146
+ { 'simple_table' => simple_table }
147
+ end
148
+ let(:to_db) do
149
+ { 'simple_table_a' => renamed_table1, 'simple_table_b' => renamed_table2 }
150
+ end
151
+
152
+ it 'should raise an ArgumentError' do
153
+ expect { Convergence::Diff.new.diff(from_db, to_db) }.to raise_error(ArgumentError)
154
+ end
155
+ end
156
+
67
157
  context 'table column are changed' do
68
158
  let(:table_from) do
69
159
  Convergence::Table.new('table1').tap do |t|
@@ -132,6 +222,23 @@ describe Convergence::DSL do
132
222
  expect(included_after_option).to eq false
133
223
  end
134
224
  end
225
+
226
+ context 'all columns are renamed' do
227
+ let(:table_to) do
228
+ Convergence::Table.new('table1').tap do |t|
229
+ t.int('id_rename', primary_key: true, renamed_from: 'id')
230
+ t.varchar('name_rename', limit: 200, null: false, renamed_from: 'name')
231
+ end
232
+ end
233
+
234
+ it 'should detect it as a change_table with rename_column, not a drop+add' do
235
+ results = Convergence::Diff.new.diff({ 'table1' => table_from }, { 'table1' => table_to })
236
+ expect(results[:add_table]).to be_empty
237
+ expect(results[:remove_table]).to be_empty
238
+ expect(results[:change_table]).not_to be_empty
239
+ expect(results[:change_table]['table1'][:rename_column]).to eq('id' => 'id_rename', 'name' => 'name_rename')
240
+ end
241
+ end
135
242
  end
136
243
  end
137
244
 
@@ -248,6 +355,66 @@ describe Convergence::DSL do
248
355
  end
249
356
  end
250
357
 
358
+ context 'rename column' do
359
+ let(:table_from) do
360
+ Convergence::Table.new('table1').tap do |t|
361
+ t.int('id', primary_key: true)
362
+ t.varchar('name', limit: 200, null: false)
363
+ end
364
+ end
365
+ let(:table_to) do
366
+ Convergence::Table.new('table1').tap do |t|
367
+ t.int('id', primary_key: true)
368
+ t.varchar('full_name', limit: 200, null: false, renamed_from: 'name')
369
+ end
370
+ end
371
+
372
+ it 'should detect it as a rename, not remove+add' do
373
+ expect(results[:rename_column]).to eq('name' => 'full_name')
374
+ expect(results[:remove_column]).to be_empty
375
+ expect(results[:add_column]).to be_empty
376
+ end
377
+ end
378
+
379
+ context 'rename column together with an unrelated change on another column' do
380
+ let(:table_from) do
381
+ Convergence::Table.new('table1').tap do |t|
382
+ t.int('id', primary_key: true)
383
+ t.varchar('name', limit: 200, null: false)
384
+ end
385
+ end
386
+ let(:table_to) do
387
+ Convergence::Table.new('table1').tap do |t|
388
+ t.int('id', primary_key: true)
389
+ t.varchar('full_name', limit: 300, null: false, renamed_from: 'name')
390
+ end
391
+ end
392
+
393
+ it 'should detect only the rename, deferring the other change to the next apply pass' do
394
+ expect(results[:rename_column]).to eq('name' => 'full_name')
395
+ expect(results[:change_column]).to be_empty
396
+ end
397
+ end
398
+
399
+ context 'rename the only column in a table' do
400
+ let(:table_from) do
401
+ Convergence::Table.new('table1').tap do |t|
402
+ t.int('id', primary_key: true)
403
+ end
404
+ end
405
+ let(:table_to) do
406
+ Convergence::Table.new('table1').tap do |t|
407
+ t.int('identifier', primary_key: true, renamed_from: 'id')
408
+ end
409
+ end
410
+
411
+ it 'should not be misdetected as removing all columns' do
412
+ expect(results[:rename_column]).to eq('id' => 'identifier')
413
+ expect(results[:remove_column]).to be_empty
414
+ expect(results[:add_column]).to be_empty
415
+ end
416
+ end
417
+
251
418
  context 'remove index' do
252
419
  let(:table_from) do
253
420
  Convergence::Table.new('table1').tap do |t|
@@ -26,6 +26,29 @@ describe Convergence::Table do
26
26
  table.decimal(dummy_column, default: 1)
27
27
  expect(table.columns[dummy_column].options[:default]).to eq(1.0)
28
28
  end
29
+
30
+ it 'should extract renamed_from into its own accessor' do
31
+ table.int(dummy_column, renamed_from: 'old_column')
32
+ expect(table.columns[dummy_column].renamed_from).to eq('old_column')
33
+ expect(table.columns[dummy_column].options).not_to have_key(:renamed_from)
34
+ end
35
+
36
+ it 'should have a nil renamed_from when not specified' do
37
+ table.int(dummy_column)
38
+ expect(table.columns[dummy_column].renamed_from).to be_nil
39
+ end
40
+ end
41
+
42
+ describe 'renamed_from' do
43
+ it 'should extract renamed_from into its own accessor' do
44
+ renamed_table = Convergence::Table.new('new_table', renamed_from: 'old_table')
45
+ expect(renamed_table.renamed_from).to eq('old_table')
46
+ expect(renamed_table.table_options).not_to have_key(:renamed_from)
47
+ end
48
+
49
+ it 'should have a nil renamed_from when not specified' do
50
+ expect(table.renamed_from).to be_nil
51
+ end
29
52
  end
30
53
 
31
54
  describe '#boolean' do
@@ -0,0 +1,28 @@
1
+ create_table "authors" do |t|
2
+ t.int "id", primary_key: true, extra: "auto_increment"
3
+ t.varchar "name", limit: 110
4
+ t.int "age"
5
+ t.datetime "created_at", null: true
6
+ t.datetime "updated_at", null: true
7
+
8
+ t.index "created_at", name: "index_authors_on_created_at"
9
+ end
10
+
11
+ create_table "papers", comment: "Paper" do |t|
12
+ t.int "id", primary_key: true, extra: "auto_increment"
13
+ t.varchar "slug", comment: "Slug"
14
+ t.varchar "title1", limit: 300, comment: "Title 1"
15
+ t.varchar "title2", limit: 300, comment: "Title 2"
16
+ t.text "notes", null: true, comment: "Description", renamed_from: "description"
17
+
18
+ t.index "slug", name: "index_papers_on_slug", unique: true
19
+ end
20
+
21
+ create_table "paper_authors", comment: "Paper Author Relation" do |t|
22
+ t.int "id", primary_key: true, extra: "auto_increment"
23
+ t.int "paper_id", comment: "Paper id"
24
+ t.int "author_id", comment: "Paper author id"
25
+
26
+ t.foreign_key "author_id", reference: "authors", reference_column: "id", name: "paper_authors_author_id_fk"
27
+ t.foreign_key "paper_id", reference: "papers", reference_column: "id", name: "paper_authors_paper_id_fk"
28
+ end
@@ -0,0 +1,28 @@
1
+ create_table "authors" do |t|
2
+ t.int "id", primary_key: true, extra: "auto_increment"
3
+ t.varchar "name", limit: 110
4
+ t.int "age"
5
+ t.datetime "created_at", null: true
6
+ t.datetime "updated_at", null: true
7
+
8
+ t.index "created_at", name: "index_authors_on_created_at"
9
+ end
10
+
11
+ create_table "papers", comment: "Paper" do |t|
12
+ t.int "id", primary_key: true, extra: "auto_increment"
13
+ t.varchar "slug", comment: "Slug"
14
+ t.varchar "title1", limit: 300, comment: "Title 1"
15
+ t.varchar "title2", limit: 300, comment: "Title 2"
16
+ t.text "description", null: true, comment: "Description"
17
+
18
+ t.index "slug", name: "index_papers_on_slug", unique: true
19
+ end
20
+
21
+ create_table "paper_author_links", comment: "Paper Author Relation", renamed_from: "paper_authors" do |t|
22
+ t.int "id", primary_key: true, extra: "auto_increment"
23
+ t.int "paper_id", comment: "Paper id"
24
+ t.int "author_id", comment: "Paper author id"
25
+
26
+ t.foreign_key "author_id", reference: "authors", reference_column: "id", name: "paper_authors_author_id_fk"
27
+ t.foreign_key "paper_id", reference: "papers", reference_column: "id", name: "paper_authors_paper_id_fk"
28
+ end
@@ -0,0 +1,26 @@
1
+ create_table "authors", collate: "utf8_general_ci", comment: "" do |t|
2
+ t.int "id", primary_key: true, extra: "auto_increment"
3
+ t.varchar "full_name", limit: 110, renamed_from: "name"
4
+ t.int "age", unsigned: true
5
+ t.datetime "created_at", null: true
6
+ t.datetime "updated_at", null: true
7
+
8
+ t.index "created_at", name: "index_authors_on_created_at"
9
+ end
10
+
11
+ create_table "papers", collate: "utf8_general_ci", comment: "Paper" do |t|
12
+ t.int "id", primary_key: true, extra: "auto_increment"
13
+ t.varchar "slug", comment: "Slug"
14
+ t.varchar "title1", limit: 300, comment: "Title 1"
15
+ t.varchar "title2", limit: 300, comment: "Title 2"
16
+ t.text "description", null: true, comment: "Description"
17
+ end
18
+
19
+ create_table "paper_authors", collate: "utf8_general_ci", comment: "Paper Author Relation" do |t|
20
+ t.int "id", primary_key: true, extra: "auto_increment"
21
+ t.int "paper_id", comment: "Paper id"
22
+ t.int "author_id", comment: "Paper author id"
23
+
24
+ t.foreign_key "author_id", reference: "authors", reference_column: "id", name: "paper_authors_author_id_fk"
25
+ t.foreign_key "paper_id", reference: "papers", reference_column: "id", name: "paper_authors_paper_id_fk"
26
+ end
@@ -0,0 +1,26 @@
1
+ create_table "authors", collate: "utf8_general_ci", comment: "" do |t|
2
+ t.int "id", primary_key: true, extra: "auto_increment"
3
+ t.varchar "name", limit: 110
4
+ t.int "age", unsigned: true
5
+ t.datetime "created_at", null: true
6
+ t.datetime "updated_at", null: true
7
+
8
+ t.index "created_at", name: "index_authors_on_created_at"
9
+ end
10
+
11
+ create_table "papers", collate: "utf8_general_ci", comment: "Paper" do |t|
12
+ t.int "id", primary_key: true, extra: "auto_increment"
13
+ t.varchar "slug", comment: "Slug"
14
+ t.varchar "title1", limit: 300, comment: "Title 1"
15
+ t.varchar "title2", limit: 300, comment: "Title 2"
16
+ t.text "description", null: true, comment: "Description"
17
+ end
18
+
19
+ create_table "paper_author_links", collate: "utf8_general_ci", comment: "Paper Author Relation", renamed_from: "paper_authors" do |t|
20
+ t.int "id", primary_key: true, extra: "auto_increment"
21
+ t.int "paper_id", comment: "Paper id"
22
+ t.int "author_id", comment: "Paper author id"
23
+
24
+ t.foreign_key "author_id", reference: "authors", reference_column: "id", name: "paper_authors_author_id_fk"
25
+ t.foreign_key "paper_id", reference: "papers", reference_column: "id", name: "paper_authors_paper_id_fk"
26
+ end
@@ -0,0 +1,28 @@
1
+ create_table "authors" do |t|
2
+ t.int "id", primary_key: true, extra: "auto_increment"
3
+ t.varchar "name", limit: 110
4
+ t.int "age"
5
+ t.datetime "created_at", null: true
6
+ t.datetime "updated_at", null: true
7
+
8
+ t.index "created_at", name: "index_authors_on_created_at"
9
+ end
10
+
11
+ create_table "papers" do |t|
12
+ t.int "id", primary_key: true, extra: "auto_increment"
13
+ t.varchar "slug", limit: 255
14
+ t.varchar "title1", limit: 300
15
+ t.varchar "title2", limit: 300
16
+ t.text "notes", null: true, renamed_from: "description"
17
+
18
+ t.index "slug", name: "index_papers_on_slug", unique: true
19
+ end
20
+
21
+ create_table "paper_authors" do |t|
22
+ t.int "id", primary_key: true, extra: "auto_increment"
23
+ t.int "paper_id"
24
+ t.int "author_id"
25
+
26
+ t.foreign_key "author_id", reference: "authors", reference_column: "id", name: "paper_authors_author_id_fk"
27
+ t.foreign_key "paper_id", reference: "papers", reference_column: "id", name: "paper_authors_paper_id_fk"
28
+ end
@@ -0,0 +1,27 @@
1
+ create_table "authors" do |t|
2
+ t.int "id", primary_key: true, extra: "auto_increment"
3
+ t.varchar "name", limit: 110
4
+ t.int "age"
5
+ t.datetime "created_at", null: true
6
+ t.datetime "updated_at", null: true
7
+
8
+ t.index "created_at", name: "index_authors_on_created_at"
9
+ end
10
+
11
+ create_table "papers" do |t|
12
+ t.int "id", primary_key: true, extra: "auto_increment"
13
+ t.varchar "slug", limit: 255
14
+ t.varchar "title1", limit: 300
15
+ t.varchar "title2", limit: 300
16
+
17
+ t.index "slug", name: "index_papers_on_slug", unique: true
18
+ end
19
+
20
+ create_table "paper_author_links", renamed_from: "paper_authors" do |t|
21
+ t.int "id", primary_key: true, extra: "auto_increment"
22
+ t.int "paper_id"
23
+ t.int "author_id"
24
+
25
+ t.foreign_key "author_id", reference: "authors", reference_column: "id", name: "paper_authors_author_id_fk"
26
+ t.foreign_key "paper_id", reference: "papers", reference_column: "id", name: "paper_authors_paper_id_fk"
27
+ end
@@ -108,6 +108,28 @@ describe 'Command::Dryrun#execute' do
108
108
  end
109
109
  end
110
110
 
111
+ describe 'rename column' do
112
+ let(:exec_dsl) { 'rename_column_to_author.schema' }
113
+
114
+ it 'should be output rename column query, not drop+add' do
115
+ result = execute(exec_dsl)
116
+ expect(result).to be_include('# ALTER TABLE `authors` RENAME COLUMN `name` TO `full_name`;')
117
+ expect(result).not_to be_include('DROP COLUMN `name`')
118
+ expect(result).not_to be_include('ADD COLUMN `full_name`')
119
+ end
120
+ end
121
+
122
+ describe 'rename table' do
123
+ let(:exec_dsl) { 'rename_table.schema' }
124
+
125
+ it 'should be output rename table query, not drop+add' do
126
+ result = execute(exec_dsl)
127
+ expect(result).to be_include('# ALTER TABLE `paper_authors` RENAME TO `paper_author_links`;')
128
+ expect(result).not_to be_include('DROP TABLE `paper_authors`')
129
+ expect(result).not_to be_include('CREATE TABLE `paper_author_links`')
130
+ end
131
+ end
132
+
111
133
  describe 'auto increment' do
112
134
  describe 'create table with auto increment option' do
113
135
  let(:exec_dsl) { 'add_table.schema' }
@@ -67,6 +67,28 @@ describe 'PostgreSQL Command::Dryrun#execute' do
67
67
  end
68
68
  end
69
69
 
70
+ describe 'rename column' do
71
+ let(:exec_dsl) { 'rename_column_on_paper.schema' }
72
+
73
+ it 'should be output rename column query, not drop+add' do
74
+ result = execute(exec_dsl)
75
+ expect(result).to be_include(%(# ALTER TABLE "papers" RENAME COLUMN "description" TO "notes";))
76
+ expect(result).not_to be_include('DROP COLUMN "description"')
77
+ expect(result).not_to be_include('ADD COLUMN "notes"')
78
+ end
79
+ end
80
+
81
+ describe 'rename table' do
82
+ let(:exec_dsl) { 'rename_table.schema' }
83
+
84
+ it 'should be output rename table query, not drop+add' do
85
+ result = execute(exec_dsl)
86
+ expect(result).to be_include(%(# ALTER TABLE "paper_authors" RENAME TO "paper_author_links";))
87
+ expect(result).not_to be_include('DROP TABLE "paper_authors"')
88
+ expect(result).not_to be_include('CREATE TABLE "paper_author_links"')
89
+ end
90
+ end
91
+
70
92
  describe 'drop foreign key' do
71
93
  let(:exec_dsl) { 'drop_foreign_key.schema' }
72
94
 
@@ -60,6 +60,28 @@ describe 'SQLite Command::Dryrun#execute' do
60
60
  end
61
61
  end
62
62
 
63
+ describe 'rename column' do
64
+ let(:exec_dsl) { 'rename_column_on_paper.schema' }
65
+
66
+ it 'should be output rename column query, not drop+add' do
67
+ result = execute(exec_dsl)
68
+ expect(result).to be_include(%(# ALTER TABLE "papers" RENAME COLUMN "description" TO "notes";))
69
+ expect(result).not_to be_include('DROP COLUMN "description"')
70
+ expect(result).not_to be_include('ADD COLUMN "notes"')
71
+ end
72
+ end
73
+
74
+ describe 'rename table' do
75
+ let(:exec_dsl) { 'rename_table.schema' }
76
+
77
+ it 'should be output rename table query, not drop+add' do
78
+ result = execute(exec_dsl)
79
+ expect(result).to be_include(%(# ALTER TABLE "paper_authors" RENAME TO "paper_author_links";))
80
+ expect(result).not_to be_include('DROP TABLE "paper_authors"')
81
+ expect(result).not_to be_include('CREATE TABLE "paper_author_links"')
82
+ end
83
+ end
84
+
63
85
  describe 'drop foreign key' do
64
86
  let(:exec_dsl) { 'drop_foreign_key.schema' }
65
87
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: convergence
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shinsuke Nishio
@@ -93,6 +93,34 @@ dependencies:
93
93
  - - ">="
94
94
  - !ruby/object:Gem::Version
95
95
  version: '0'
96
+ - !ruby/object:Gem::Dependency
97
+ name: logger
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
110
+ - !ruby/object:Gem::Dependency
111
+ name: benchmark
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: '0'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: '0'
96
124
  - !ruby/object:Gem::Dependency
97
125
  name: rake
98
126
  requirement: !ruby/object:Gem::Requirement
@@ -263,14 +291,20 @@ files:
263
291
  - spec/fixtures/postgres/drop_foreign_key.schema
264
292
  - spec/fixtures/postgres/drop_table.schema
265
293
  - spec/fixtures/postgres/remove_columns_to_paper.schema
294
+ - spec/fixtures/postgres/rename_column_on_paper.schema
295
+ - spec/fixtures/postgres/rename_table.schema
266
296
  - spec/fixtures/postgres_test_db.sql
267
297
  - spec/fixtures/remove_columns_to_paper.schema
298
+ - spec/fixtures/rename_column_to_author.schema
299
+ - spec/fixtures/rename_table.schema
268
300
  - spec/fixtures/sqlite/add_columns_to_paper.schema
269
301
  - spec/fixtures/sqlite/add_table.schema
270
302
  - spec/fixtures/sqlite/change_columns_to_paper.schema
271
303
  - spec/fixtures/sqlite/drop_foreign_key.schema
272
304
  - spec/fixtures/sqlite/drop_table.schema
273
305
  - spec/fixtures/sqlite/remove_columns_to_paper.schema
306
+ - spec/fixtures/sqlite/rename_column_on_paper.schema
307
+ - spec/fixtures/sqlite/rename_table.schema
274
308
  - spec/fixtures/sqlite_test_db.sql
275
309
  - spec/fixtures/test_db.sql
276
310
  - spec/integrations/command_diff.rb
@@ -330,14 +364,20 @@ test_files:
330
364
  - spec/fixtures/postgres/drop_foreign_key.schema
331
365
  - spec/fixtures/postgres/drop_table.schema
332
366
  - spec/fixtures/postgres/remove_columns_to_paper.schema
367
+ - spec/fixtures/postgres/rename_column_on_paper.schema
368
+ - spec/fixtures/postgres/rename_table.schema
333
369
  - spec/fixtures/postgres_test_db.sql
334
370
  - spec/fixtures/remove_columns_to_paper.schema
371
+ - spec/fixtures/rename_column_to_author.schema
372
+ - spec/fixtures/rename_table.schema
335
373
  - spec/fixtures/sqlite/add_columns_to_paper.schema
336
374
  - spec/fixtures/sqlite/add_table.schema
337
375
  - spec/fixtures/sqlite/change_columns_to_paper.schema
338
376
  - spec/fixtures/sqlite/drop_foreign_key.schema
339
377
  - spec/fixtures/sqlite/drop_table.schema
340
378
  - spec/fixtures/sqlite/remove_columns_to_paper.schema
379
+ - spec/fixtures/sqlite/rename_column_on_paper.schema
380
+ - spec/fixtures/sqlite/rename_table.schema
341
381
  - spec/fixtures/sqlite_test_db.sql
342
382
  - spec/fixtures/test_db.sql
343
383
  - spec/integrations/command_diff.rb