declare_schema 0.8.0.pre.6 → 0.10.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/declare_schema_build.yml +1 -1
- data/CHANGELOG.md +28 -1
- data/Gemfile.lock +1 -1
- data/README.md +91 -13
- data/lib/declare_schema.rb +46 -0
- data/lib/declare_schema/dsl.rb +39 -0
- data/lib/declare_schema/extensions/active_record/fields_declaration.rb +23 -4
- data/lib/declare_schema/model.rb +51 -59
- data/lib/declare_schema/model/field_spec.rb +11 -8
- data/lib/declare_schema/model/foreign_key_definition.rb +4 -8
- data/lib/declare_schema/model/habtm_model_shim.rb +1 -1
- data/lib/declare_schema/model/index_definition.rb +0 -19
- data/lib/declare_schema/schema_change/all.rb +22 -0
- data/lib/declare_schema/schema_change/base.rb +45 -0
- data/lib/declare_schema/schema_change/column_add.rb +27 -0
- data/lib/declare_schema/schema_change/column_change.rb +32 -0
- data/lib/declare_schema/schema_change/column_remove.rb +20 -0
- data/lib/declare_schema/schema_change/column_rename.rb +23 -0
- data/lib/declare_schema/schema_change/foreign_key_add.rb +25 -0
- data/lib/declare_schema/schema_change/foreign_key_remove.rb +20 -0
- data/lib/declare_schema/schema_change/index_add.rb +33 -0
- data/lib/declare_schema/schema_change/index_remove.rb +20 -0
- data/lib/declare_schema/schema_change/primary_key_change.rb +33 -0
- data/lib/declare_schema/schema_change/table_add.rb +37 -0
- data/lib/declare_schema/schema_change/table_change.rb +36 -0
- data/lib/declare_schema/schema_change/table_remove.rb +22 -0
- data/lib/declare_schema/schema_change/table_rename.rb +22 -0
- data/lib/declare_schema/version.rb +1 -1
- data/lib/generators/declare_schema/migration/migrator.rb +189 -202
- data/lib/generators/declare_schema/support/model.rb +4 -4
- data/spec/lib/declare_schema/api_spec.rb +7 -7
- data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +41 -15
- data/spec/lib/declare_schema/field_spec_spec.rb +50 -6
- data/spec/lib/declare_schema/generator_spec.rb +3 -3
- data/spec/lib/declare_schema/interactive_primary_key_spec.rb +116 -26
- data/spec/lib/declare_schema/migration_generator_spec.rb +1891 -845
- data/spec/lib/declare_schema/model/column_spec.rb +47 -17
- data/spec/lib/declare_schema/model/foreign_key_definition_spec.rb +134 -57
- data/spec/lib/declare_schema/model/habtm_model_shim_spec.rb +3 -3
- data/spec/lib/declare_schema/model/index_definition_spec.rb +188 -77
- data/spec/lib/declare_schema/model/table_options_definition_spec.rb +75 -11
- data/spec/lib/declare_schema/schema_change/base_spec.rb +75 -0
- data/spec/lib/declare_schema/schema_change/column_add_spec.rb +30 -0
- data/spec/lib/declare_schema/schema_change/column_change_spec.rb +33 -0
- data/spec/lib/declare_schema/schema_change/column_remove_spec.rb +30 -0
- data/spec/lib/declare_schema/schema_change/column_rename_spec.rb +28 -0
- data/spec/lib/declare_schema/schema_change/foreign_key_add_spec.rb +29 -0
- data/spec/lib/declare_schema/schema_change/foreign_key_remove_spec.rb +29 -0
- data/spec/lib/declare_schema/schema_change/index_add_spec.rb +56 -0
- data/spec/lib/declare_schema/schema_change/index_remove_spec.rb +29 -0
- data/spec/lib/declare_schema/schema_change/primary_key_change_spec.rb +69 -0
- data/spec/lib/declare_schema/schema_change/table_add_spec.rb +50 -0
- data/spec/lib/declare_schema/schema_change/table_change_spec.rb +30 -0
- data/spec/lib/declare_schema/schema_change/table_remove_spec.rb +27 -0
- data/spec/lib/declare_schema/schema_change/table_rename_spec.rb +27 -0
- data/spec/lib/declare_schema_spec.rb +101 -0
- data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +71 -13
- data/spec/support/acceptance_spec_helpers.rb +2 -2
- metadata +33 -3
- data/test_responses.txt +0 -2
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module DeclareSchema
|
6
|
+
module SchemaChange
|
7
|
+
class TableAdd < Base
|
8
|
+
def initialize(table_name, fields, create_table_options, sql_options: nil)
|
9
|
+
@table_name = table_name
|
10
|
+
fields.all? do |type, name, options|
|
11
|
+
type.is_a?(Symbol) && name.is_a?(Symbol) && options.is_a?(Hash)
|
12
|
+
end or raise ArgumentError, "fields must be Array(Array(Symbol, Symbol, Hash)); got #{fields.inspect}"
|
13
|
+
@fields = fields
|
14
|
+
@create_table_options = create_table_options
|
15
|
+
@create_table_options = @create_table_options.merge(options: sql_options) if sql_options.present?
|
16
|
+
end
|
17
|
+
|
18
|
+
def up_command
|
19
|
+
longest_field_type_length = @fields.map { |type, _name, _option| type.to_s.length }.max
|
20
|
+
|
21
|
+
<<~EOS.strip
|
22
|
+
create_table #{[@table_name.to_sym.inspect, *self.class.format_options(@create_table_options)].join(', ')} do |t|
|
23
|
+
#{@fields.map do |type, name, options|
|
24
|
+
padded_type = format("%-*s", longest_field_type_length, type)
|
25
|
+
args = [name.inspect, *self.class.format_options(options)].join(', ')
|
26
|
+
" t.#{padded_type} #{args}"
|
27
|
+
end.join("\n")}
|
28
|
+
end
|
29
|
+
EOS
|
30
|
+
end
|
31
|
+
|
32
|
+
def down_command
|
33
|
+
"drop_table #{@table_name.to_sym.inspect}"
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module DeclareSchema
|
6
|
+
module SchemaChange
|
7
|
+
class TableChange < Base
|
8
|
+
def initialize(table_name, old_options, new_options)
|
9
|
+
@table_name = table_name
|
10
|
+
@old_options = old_options
|
11
|
+
@new_options = new_options
|
12
|
+
end
|
13
|
+
|
14
|
+
def up_command
|
15
|
+
alter_table(@table_name, @new_options)
|
16
|
+
end
|
17
|
+
|
18
|
+
def down_command
|
19
|
+
alter_table(@table_name, @old_options)
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
TABLE_OPTIONS_TO_SQL_MAPPINGS = {
|
25
|
+
charset: 'CHARACTER SET',
|
26
|
+
collation: 'COLLATE'
|
27
|
+
}.freeze
|
28
|
+
|
29
|
+
def alter_table(table_name, options)
|
30
|
+
sql_options = options.map { |key, value| [TABLE_OPTIONS_TO_SQL_MAPPINGS[key], value] }
|
31
|
+
statement = "ALTER TABLE #{ActiveRecord::Base.connection.quote_table_name(table_name)} #{sql_options.join(' ')}"
|
32
|
+
"execute #{statement.inspect}"
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module DeclareSchema
|
6
|
+
module SchemaChange
|
7
|
+
class TableRemove < Base
|
8
|
+
def initialize(table_name, add_table_back)
|
9
|
+
@table_name = table_name
|
10
|
+
@add_table_back = add_table_back
|
11
|
+
end
|
12
|
+
|
13
|
+
def up_command
|
14
|
+
"drop_table #{@table_name.to_sym.inspect}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def down_command
|
18
|
+
@add_table_back
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module DeclareSchema
|
6
|
+
module SchemaChange
|
7
|
+
class TableRename < Base
|
8
|
+
def initialize(old_name, new_name)
|
9
|
+
@old_name = old_name
|
10
|
+
@new_name = new_name
|
11
|
+
end
|
12
|
+
|
13
|
+
def up_command
|
14
|
+
"rename_table #{@old_name.to_sym.inspect}, #{@new_name.to_sym.inspect}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def down_command
|
18
|
+
"rename_table #{@new_name.to_sym.inspect}, #{@old_name.to_sym.inspect}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'active_record'
|
4
4
|
require 'active_record/connection_adapters/abstract_adapter'
|
5
|
+
require 'declare_schema/schema_change/all'
|
5
6
|
|
6
7
|
module Generators
|
7
8
|
module DeclareSchema
|
@@ -9,29 +10,14 @@ module Generators
|
|
9
10
|
class Migrator
|
10
11
|
class Error < RuntimeError; end
|
11
12
|
|
12
|
-
DEFAULT_CHARSET = "utf8mb4"
|
13
|
-
DEFAULT_COLLATION = "utf8mb4_bin"
|
14
|
-
|
15
13
|
@ignore_models = []
|
16
14
|
@ignore_tables = []
|
17
15
|
@before_generating_migration_callback = nil
|
18
16
|
@active_record_class = ActiveRecord::Base
|
19
|
-
@default_charset = DEFAULT_CHARSET
|
20
|
-
@default_collation = DEFAULT_COLLATION
|
21
17
|
|
22
18
|
class << self
|
23
|
-
attr_accessor :ignore_models, :ignore_tables
|
24
|
-
attr_reader :active_record_class, :
|
25
|
-
|
26
|
-
def default_charset=(charset)
|
27
|
-
charset.is_a?(String) or raise ArgumentError, "charset must be a string (got #{charset.inspect})"
|
28
|
-
@default_charset = charset
|
29
|
-
end
|
30
|
-
|
31
|
-
def default_collation=(collation)
|
32
|
-
collation.is_a?(String) or raise ArgumentError, "collation must be a string (got #{collation.inspect})"
|
33
|
-
@default_collation = collation
|
34
|
-
end
|
19
|
+
attr_accessor :ignore_models, :ignore_tables
|
20
|
+
attr_reader :active_record_class, :before_generating_migration_callback
|
35
21
|
|
36
22
|
def active_record_class
|
37
23
|
@active_record_class.is_a?(Class) or @active_record_class = @active_record_class.to_s.constantize
|
@@ -39,9 +25,7 @@ module Generators
|
|
39
25
|
end
|
40
26
|
|
41
27
|
def run(renames = {})
|
42
|
-
|
43
|
-
g.renames = renames
|
44
|
-
g.generate
|
28
|
+
Migrator.new(renames: renames).generate
|
45
29
|
end
|
46
30
|
|
47
31
|
def default_migration_name
|
@@ -58,16 +42,17 @@ module Generators
|
|
58
42
|
block or raise ArgumentError, 'A block is required when setting the before_generating_migration callback'
|
59
43
|
@before_generating_migration_callback = block
|
60
44
|
end
|
45
|
+
|
46
|
+
delegate :default_charset=, :default_collation=, :default_charset, :default_collation, to: ::DeclareSchema
|
47
|
+
deprecate :default_charset=, :default_collation=, :default_charset, :default_collation, deprecator: ActiveSupport::Deprecation.new('1.0', 'declare_schema')
|
61
48
|
end
|
62
49
|
|
63
|
-
def initialize(ambiguity_resolver = {})
|
50
|
+
def initialize(ambiguity_resolver = {}, renames: nil)
|
64
51
|
@ambiguity_resolver = ambiguity_resolver
|
65
52
|
@drops = []
|
66
|
-
@renames =
|
53
|
+
@renames = renames
|
67
54
|
end
|
68
55
|
|
69
|
-
attr_accessor :renames
|
70
|
-
|
71
56
|
def load_rails_models
|
72
57
|
ActiveRecord::Migration.verbose = false
|
73
58
|
|
@@ -121,19 +106,24 @@ module Generators
|
|
121
106
|
# return a hash of table renames and modifies the passed arrays so
|
122
107
|
# that renamed tables are no longer listed as to_create or to_drop
|
123
108
|
def extract_table_renames!(to_create, to_drop)
|
124
|
-
if renames
|
109
|
+
if @renames
|
125
110
|
# A hash of table renames has been provided
|
126
111
|
|
127
112
|
to_rename = {}
|
128
|
-
renames.
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
if to_create.delete(new_name.to_s) && to_drop.delete(old_name.to_s)
|
133
|
-
to_rename[old_name.to_s] = new_name.to_s
|
134
|
-
else
|
135
|
-
raise Error, "Invalid table rename specified: #{old_name} => #{new_name}"
|
113
|
+
@renames.each do |old_name, new_name|
|
114
|
+
if new_name.is_a?(Hash)
|
115
|
+
new_name = new_name[:table_name]
|
136
116
|
end
|
117
|
+
new_name or next
|
118
|
+
|
119
|
+
old_name = old_name.to_s
|
120
|
+
new_name = new_name.to_s
|
121
|
+
|
122
|
+
to_create.delete(new_name) or raise Error,
|
123
|
+
"Rename specified new name: #{new_name.inspect} but it was not in the `to_create` list"
|
124
|
+
to_drop.delete(old_name) or raise Error,
|
125
|
+
"Rename specified old name: #{old_name.inspect} but it was not in the `to_drop` list"
|
126
|
+
to_rename[old_name] = new_name
|
137
127
|
end
|
138
128
|
to_rename
|
139
129
|
|
@@ -145,18 +135,22 @@ module Generators
|
|
145
135
|
end
|
146
136
|
end
|
147
137
|
|
138
|
+
# return a hash of column renames and modifies the passed arrays so
|
139
|
+
# that renamed columns are no longer listed as to_create or to_drop
|
148
140
|
def extract_column_renames!(to_add, to_remove, table_name)
|
149
|
-
if renames
|
141
|
+
if @renames
|
150
142
|
to_rename = {}
|
151
|
-
if (column_renames = renames
|
152
|
-
# A hash of
|
153
|
-
|
154
|
-
column_renames.
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
143
|
+
if (column_renames = @renames[table_name.to_sym])
|
144
|
+
# A hash of column renames has been provided
|
145
|
+
|
146
|
+
column_renames.each do |old_name, new_name|
|
147
|
+
old_name = old_name.to_s
|
148
|
+
new_name = new_name.to_s
|
149
|
+
to_add.delete(new_name) or raise Error,
|
150
|
+
"Rename specified new name: #{new_name.inspect} but it was not in the `to_add` list for table #{table_name}"
|
151
|
+
to_remove.delete(old_name) or raise Error,
|
152
|
+
"Rename specified old name: #{old_name.inspect} but it was not in the `to_remove` list for table #{table_name}"
|
153
|
+
to_rename[old_name] = new_name
|
160
154
|
end
|
161
155
|
end
|
162
156
|
to_rename
|
@@ -213,84 +207,103 @@ module Generators
|
|
213
207
|
to_rename = extract_table_renames!(to_create, to_drop)
|
214
208
|
|
215
209
|
renames = to_rename.map do |old_name, new_name|
|
216
|
-
|
217
|
-
end
|
218
|
-
undo_renames = to_rename.map do |old_name, new_name|
|
219
|
-
"rename_table :#{new_name}, :#{old_name}"
|
220
|
-
end * "\n"
|
210
|
+
::DeclareSchema::SchemaChange::TableRename.new(old_name, new_name)
|
211
|
+
end
|
221
212
|
|
222
213
|
drops = to_drop.map do |t|
|
223
|
-
|
224
|
-
end
|
225
|
-
undo_drops = to_drop.map do |t|
|
226
|
-
add_table_back(t)
|
227
|
-
end * "\n\n"
|
214
|
+
::DeclareSchema::SchemaChange::TableRemove.new(t, add_table_back(t))
|
215
|
+
end
|
228
216
|
|
229
217
|
creates = to_create.map do |t|
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
218
|
+
model = models_by_table_name[t]
|
219
|
+
disable_auto_increment = model.try(:disable_auto_increment)
|
220
|
+
|
221
|
+
primary_key_definition =
|
222
|
+
if disable_auto_increment
|
223
|
+
[:integer, :id, limit: 8, auto_increment: false, primary_key: true]
|
224
|
+
end
|
225
|
+
|
226
|
+
field_definitions = model.field_specs.values.sort_by(&:position).map do |f|
|
227
|
+
[f.type, f.name, f.sql_options]
|
228
|
+
end
|
229
|
+
|
230
|
+
table_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, table_options_for_model(model))
|
231
|
+
table_options = create_table_options(model, disable_auto_increment)
|
232
|
+
|
233
|
+
table_add = ::DeclareSchema::SchemaChange::TableAdd.new(t,
|
234
|
+
Array(primary_key_definition) + field_definitions,
|
235
|
+
table_options,
|
236
|
+
sql_options: table_options_definition.settings)
|
237
|
+
[
|
238
|
+
table_add,
|
239
|
+
*Array((create_indexes(model) if ::DeclareSchema.default_generate_indexing)),
|
240
|
+
*Array((create_constraints(model) if ::DeclareSchema.default_generate_foreign_keys))
|
241
|
+
]
|
242
|
+
end
|
235
243
|
|
236
244
|
changes = []
|
237
|
-
undo_changes = []
|
238
245
|
index_changes = []
|
239
|
-
undo_index_changes = []
|
240
246
|
fk_changes = []
|
241
|
-
undo_fk_changes = []
|
242
247
|
table_options_changes = []
|
243
|
-
undo_table_options_changes = []
|
244
248
|
|
245
249
|
to_change.each do |t|
|
246
250
|
model = models_by_table_name[t]
|
247
251
|
table = to_rename.key(t) || model.table_name
|
248
252
|
if table.in?(db_tables)
|
249
|
-
change,
|
253
|
+
change, index_change, fk_change, table_options_change = change_table(model, table)
|
250
254
|
changes << change
|
251
|
-
undo_changes << undo
|
252
255
|
index_changes << index_change
|
253
|
-
undo_index_changes << undo_index
|
254
256
|
fk_changes << fk_change
|
255
|
-
undo_fk_changes << undo_fk
|
256
257
|
table_options_changes << table_options_change
|
257
|
-
undo_table_options_changes << undo_table_options_change
|
258
258
|
end
|
259
259
|
end
|
260
260
|
|
261
|
-
|
262
|
-
down = [undo_changes, undo_renames, undo_drops, undo_creates, undo_index_changes, undo_fk_changes, undo_table_options_changes].flatten.reject(&:blank?) * "\n\n"
|
261
|
+
migration_commands = [renames, drops, creates, changes, index_changes, fk_changes, table_options_changes].flatten
|
263
262
|
|
264
|
-
|
263
|
+
ordered_migration_commands = order_migrations(migration_commands)
|
264
|
+
|
265
|
+
up_and_down_migrations(ordered_migration_commands)
|
265
266
|
end
|
266
267
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
268
|
+
MIGRATION_ORDER = %w[ TableRename
|
269
|
+
TableAdd
|
270
|
+
TableChange
|
271
|
+
ColumnAdd
|
272
|
+
ColumnRename
|
273
|
+
ColumnChange
|
274
|
+
PrimaryKeyChange
|
275
|
+
IndexAdd
|
276
|
+
ForeignKeyAdd
|
277
|
+
ForeignKeyRemove
|
278
|
+
IndexRemove
|
279
|
+
ColumnRemove
|
280
|
+
TableRemove ]
|
275
281
|
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
282
|
+
def order_migrations(migration_commands)
|
283
|
+
migration_commands.each_with_index.sort_by do |command, index|
|
284
|
+
command_type = command.class.name.gsub(/.*::/, '')
|
285
|
+
priority = MIGRATION_ORDER.index(command_type) or raise "#{command_type.inspect} not found in #{MIGRATION_ORDER.inspect}"
|
286
|
+
[priority, index] # index keeps the sort stable in case of a tie
|
287
|
+
end.map(&:first) # remove the index
|
288
|
+
end
|
289
|
+
|
290
|
+
private
|
291
|
+
|
292
|
+
def up_and_down_migrations(migration_commands)
|
293
|
+
up = migration_commands.map(&:up ).select(&:present?)
|
294
|
+
down = migration_commands.map(&:down).select(&:present?).reverse
|
280
295
|
|
281
|
-
|
282
|
-
#{create_indexes(model).join("\n") unless Migrator.disable_indexing}
|
283
|
-
#{create_constraints(model).join("\n") unless Migrator.disable_indexing}
|
284
|
-
EOS
|
296
|
+
[up * "\n\n", down * "\n\n"]
|
285
297
|
end
|
286
298
|
|
287
299
|
def create_table_options(model, disable_auto_increment)
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
300
|
+
primary_key = model._defined_primary_key
|
301
|
+
if primary_key.blank? || disable_auto_increment
|
302
|
+
{ id: false }
|
303
|
+
elsif primary_key == "id"
|
304
|
+
{ id: :bigint }
|
292
305
|
else
|
293
|
-
|
306
|
+
{ primary_key: primary_key.to_sym }
|
294
307
|
end
|
295
308
|
end
|
296
309
|
|
@@ -299,42 +312,42 @@ module Generators
|
|
299
312
|
{}
|
300
313
|
else
|
301
314
|
{
|
302
|
-
charset: model.table_options[:charset] ||
|
303
|
-
collation: model.table_options[:collation] ||
|
315
|
+
charset: model.table_options[:charset] || ::DeclareSchema.default_charset,
|
316
|
+
collation: model.table_options[:collation] || ::DeclareSchema.default_collation
|
304
317
|
}
|
305
318
|
end
|
306
319
|
end
|
307
320
|
|
321
|
+
# TODO: TECH-5338: optimize that index doesn't need to be dropped on undo since entire table will be dropped
|
308
322
|
def create_indexes(model)
|
309
|
-
model.index_definitions.map
|
323
|
+
model.index_definitions.map do |i|
|
324
|
+
::DeclareSchema::SchemaChange::IndexAdd.new(model.table_name, i.columns, unique: i.unique, where: i.where, name: i.name)
|
325
|
+
end
|
310
326
|
end
|
311
327
|
|
312
328
|
def create_constraints(model)
|
313
|
-
model.constraint_specs.map
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
options = field_spec.sql_options.merge(fk_field_options(field_spec.model, field_spec.name))
|
318
|
-
args = [field_spec.name.inspect] + format_options(options.compact)
|
319
|
-
format("t.%-*s %s", field_name_width, field_spec.type, args.join(', '))
|
329
|
+
model.constraint_specs.map do |fk|
|
330
|
+
::DeclareSchema::SchemaChange::ForeignKeyAdd.new(fk.child_table_name, fk.parent_table_name,
|
331
|
+
column_name: fk.foreign_key_name, name: fk.constraint_name)
|
332
|
+
end
|
320
333
|
end
|
321
334
|
|
322
335
|
def change_table(model, current_table_name)
|
323
336
|
new_table_name = model.table_name
|
324
337
|
|
325
338
|
db_columns = model.connection.columns(current_table_name).index_by(&:name)
|
326
|
-
key_missing = db_columns[model.
|
327
|
-
if model.
|
328
|
-
db_columns.delete(model.
|
339
|
+
key_missing = db_columns[model._defined_primary_key].nil? && model._defined_primary_key.present?
|
340
|
+
if model._defined_primary_key.present?
|
341
|
+
db_columns.delete(model._defined_primary_key)
|
329
342
|
end
|
330
343
|
|
331
344
|
model_column_names = model.field_specs.keys.map(&:to_s)
|
332
345
|
db_column_names = db_columns.keys.map(&:to_s)
|
333
346
|
|
334
347
|
to_add = model_column_names - db_column_names
|
335
|
-
to_add += [model.
|
348
|
+
to_add += [model._defined_primary_key] if key_missing && model._defined_primary_key.present?
|
336
349
|
to_remove = db_column_names - model_column_names
|
337
|
-
to_remove -= [model.
|
350
|
+
to_remove -= [model._defined_primary_key.to_sym] if model._defined_primary_key.present?
|
338
351
|
|
339
352
|
to_rename = extract_column_renames!(to_add, to_remove, new_table_name)
|
340
353
|
|
@@ -343,37 +356,28 @@ module Generators
|
|
343
356
|
to_change = db_column_names & model_column_names
|
344
357
|
|
345
358
|
renames = to_rename.map do |old_name, new_name|
|
346
|
-
|
347
|
-
end
|
348
|
-
undo_renames = to_rename.map do |old_name, new_name|
|
349
|
-
"rename_column :#{new_table_name}, :#{new_name}, :#{old_name}"
|
359
|
+
::DeclareSchema::SchemaChange::ColumnRename.new(new_table_name, old_name, new_name)
|
350
360
|
end
|
351
361
|
|
352
|
-
to_add
|
362
|
+
to_add.sort_by! { |c| model.field_specs[c]&.position || 0 }
|
363
|
+
|
353
364
|
adds = to_add.map do |c|
|
354
|
-
|
365
|
+
type, options =
|
355
366
|
if (spec = model.field_specs[c])
|
356
|
-
|
357
|
-
[":#{spec.type}", *format_options(options.compact)]
|
367
|
+
[spec.type, spec.sql_options.merge(fk_field_options(model, c)).compact]
|
358
368
|
else
|
359
|
-
[
|
369
|
+
[:integer, {}]
|
360
370
|
end
|
361
|
-
|
362
|
-
end
|
363
|
-
undo_adds = to_add.map do |c|
|
364
|
-
"remove_column :#{new_table_name}, :#{c}"
|
371
|
+
::DeclareSchema::SchemaChange::ColumnAdd.new(new_table_name, c, type, options)
|
365
372
|
end
|
366
373
|
|
367
374
|
removes = to_remove.map do |c|
|
368
|
-
|
369
|
-
|
370
|
-
undo_removes = to_remove.map do |c|
|
371
|
-
add_column_back(model, current_table_name, c)
|
375
|
+
old_type, old_options = add_column_back(model, current_table_name, c)
|
376
|
+
::DeclareSchema::SchemaChange::ColumnRemove.new(new_table_name, c, old_type, old_options)
|
372
377
|
end
|
373
378
|
|
374
379
|
old_names = to_rename.invert
|
375
380
|
changes = []
|
376
|
-
undo_changes = []
|
377
381
|
to_change.each do |col_name_to_change|
|
378
382
|
orig_col_name = old_names[col_name_to_change] || col_name_to_change
|
379
383
|
column = db_columns[orig_col_name] or raise "failed to find column info for #{orig_col_name.inspect}"
|
@@ -385,36 +389,33 @@ module Generators
|
|
385
389
|
|
386
390
|
if !::DeclareSchema::Model::Column.equivalent_schema_attributes?(normalized_schema_attrs, col_attrs)
|
387
391
|
type = normalized_schema_attrs.delete(:type) or raise "no :type found in #{normalized_schema_attrs.inspect}"
|
388
|
-
|
389
|
-
|
390
|
-
|
392
|
+
old_type, old_options = change_column_back(model, current_table_name, orig_col_name)
|
393
|
+
changes << ::DeclareSchema::SchemaChange::ColumnChange.new(new_table_name, col_name_to_change,
|
394
|
+
new_type: type, new_options: normalized_schema_attrs,
|
395
|
+
old_type: old_type, old_options: old_options)
|
391
396
|
end
|
392
397
|
end
|
393
398
|
|
394
|
-
index_changes
|
395
|
-
fk_changes
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
table_options_changes
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
[(renames + adds + removes + changes)
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
fk_changes * "\n",
|
411
|
-
undo_fk_changes * "\n",
|
412
|
-
table_options_changes * "\n",
|
413
|
-
undo_table_options_changes * "\n"]
|
399
|
+
index_changes = change_indexes(model, current_table_name, to_rename)
|
400
|
+
fk_changes = if ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
|
401
|
+
[]
|
402
|
+
else
|
403
|
+
change_foreign_key_constraints(model, current_table_name)
|
404
|
+
end
|
405
|
+
table_options_changes = if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
|
406
|
+
change_table_options(model, current_table_name)
|
407
|
+
else
|
408
|
+
[]
|
409
|
+
end
|
410
|
+
|
411
|
+
[(renames + adds + removes + changes),
|
412
|
+
index_changes,
|
413
|
+
fk_changes,
|
414
|
+
table_options_changes]
|
414
415
|
end
|
415
416
|
|
416
|
-
def change_indexes(model, old_table_name,
|
417
|
-
|
417
|
+
def change_indexes(model, old_table_name, to_rename)
|
418
|
+
::DeclareSchema.default_generate_indexing or return []
|
418
419
|
|
419
420
|
new_table_name = model.table_name
|
420
421
|
existing_indexes = ::DeclareSchema::Model::IndexDefinition.for_model(model, old_table_name)
|
@@ -426,69 +427,54 @@ module Generators
|
|
426
427
|
end
|
427
428
|
end || i
|
428
429
|
end
|
429
|
-
|
430
|
-
|
431
|
-
|
430
|
+
existing_primary_keys, existing_indexes_without_primary_key = existing_indexes.partition { |i| i.primary_key? }
|
431
|
+
defined_primary_keys, model_indexes_without_primary_key = model_indexes.partition { |i| i.primary_key? }
|
432
|
+
existing_primary_keys.size <= 1 or raise "too many existing primary keys! #{existing_primary_keys.inspect}"
|
433
|
+
defined_primary_keys.size <= 1 or raise "too many defined primary keys! #{defined_primary_keys.inspect}"
|
434
|
+
existing_primary_key = existing_primary_keys.first
|
435
|
+
defined_primary_key = defined_primary_keys.first
|
436
|
+
|
437
|
+
existing_primary_key_columns = (existing_primary_key&.columns || []).map { |col_name| to_rename[col_name] || col_name }
|
438
|
+
|
439
|
+
if !ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
|
440
|
+
change_primary_key =
|
441
|
+
if (existing_primary_key || defined_primary_key) &&
|
442
|
+
existing_primary_key_columns != defined_primary_key&.columns
|
443
|
+
::DeclareSchema::SchemaChange::PrimaryKeyChange.new(new_table_name, existing_primary_key_columns, defined_primary_key&.columns)
|
444
|
+
end
|
432
445
|
end
|
433
|
-
model_has_primary_key = model_indexes.any? { |i| i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME }
|
434
446
|
|
435
|
-
|
436
|
-
|
437
|
-
undo_add_indexes << drop_index(old_table_name, i.name) unless i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME
|
438
|
-
i.to_add_statement(new_table_name, existing_has_primary_key)
|
447
|
+
drop_indexes = (existing_indexes_without_primary_key - model_indexes_without_primary_key).map do |i|
|
448
|
+
::DeclareSchema::SchemaChange::IndexRemove.new(new_table_name, i.columns, unique: i.unique, where: i.where, name: i.name)
|
439
449
|
end
|
440
|
-
undo_drop_indexes = []
|
441
|
-
drop_indexes = (existing_indexes - model_indexes).map do |i|
|
442
|
-
undo_drop_indexes << i.to_add_statement(old_table_name, model_has_primary_key)
|
443
|
-
drop_index(new_table_name, i.name) unless i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME
|
444
|
-
end.compact
|
445
450
|
|
446
|
-
|
447
|
-
|
448
|
-
|
451
|
+
add_indexes = (model_indexes_without_primary_key - existing_indexes_without_primary_key).map do |i|
|
452
|
+
::DeclareSchema::SchemaChange::IndexAdd.new(new_table_name, i.columns, unique: i.unique, where: i.where, name: i.name)
|
453
|
+
end
|
449
454
|
|
450
|
-
|
451
|
-
|
452
|
-
# for why the rescue exists
|
453
|
-
"remove_index :#{table}, name: :#{name} rescue ActiveRecord::StatementInvalid"
|
455
|
+
# the order is important here - adding a :unique, for instance needs to remove then add
|
456
|
+
[Array(change_primary_key) + drop_indexes + add_indexes]
|
454
457
|
end
|
455
458
|
|
456
459
|
def change_foreign_key_constraints(model, old_table_name)
|
457
460
|
ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/) and raise ArgumentError, 'SQLite does not support foreign keys'
|
458
|
-
|
461
|
+
::DeclareSchema.default_generate_foreign_keys or return []
|
459
462
|
|
460
|
-
new_table_name = model.table_name
|
461
463
|
existing_fks = ::DeclareSchema::Model::ForeignKeyDefinition.for_model(model, old_table_name)
|
462
464
|
model_fks = model.constraint_specs
|
463
465
|
|
464
|
-
undo_add_fks = []
|
465
|
-
add_fks = (model_fks - existing_fks).map do |fk|
|
466
|
-
# next if fk.parent.constantize.abstract_class || fk.parent == fk.model.class_name
|
467
|
-
undo_add_fks << remove_foreign_key(old_table_name, fk.constraint_name)
|
468
|
-
fk.to_add_statement
|
469
|
-
end
|
470
|
-
|
471
|
-
undo_drop_fks = []
|
472
466
|
drop_fks = (existing_fks - model_fks).map do |fk|
|
473
|
-
|
474
|
-
|
467
|
+
::DeclareSchema::SchemaChange::ForeignKeyRemove.new(fk.child_table_name, fk.parent_table_name,
|
468
|
+
column_name: fk.foreign_key_name, name: fk.constraint_name)
|
475
469
|
end
|
476
470
|
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
"remove_foreign_key(#{old_table_name.inspect}, name: #{fk_name.to_s.inspect})"
|
482
|
-
end
|
483
|
-
|
484
|
-
def format_options(options)
|
485
|
-
options.map do |k, v|
|
486
|
-
if k.is_a?(Symbol)
|
487
|
-
"#{k}: #{v.inspect}"
|
488
|
-
else
|
489
|
-
"#{k.inspect} => #{v.inspect}"
|
490
|
-
end
|
471
|
+
add_fks = (model_fks - existing_fks).map do |fk|
|
472
|
+
# next if fk.parent.constantize.abstract_class || fk.parent == fk.model.class_name
|
473
|
+
::DeclareSchema::SchemaChange::ForeignKeyAdd.new(fk.child_table_name, fk.parent_table_name,
|
474
|
+
column_name: fk.foreign_key_name, name: fk.constraint_name)
|
491
475
|
end
|
476
|
+
|
477
|
+
[drop_fks + add_fks]
|
492
478
|
end
|
493
479
|
|
494
480
|
def fk_field_options(model, field_name)
|
@@ -517,11 +503,12 @@ module Generators
|
|
517
503
|
new_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, table_options_for_model(model))
|
518
504
|
|
519
505
|
if old_options_definition.equivalent?(new_options_definition)
|
520
|
-
[
|
506
|
+
[]
|
521
507
|
else
|
522
508
|
[
|
523
|
-
|
524
|
-
|
509
|
+
::DeclareSchema::SchemaChange::TableChange.new(current_table_name,
|
510
|
+
old_options_definition.settings,
|
511
|
+
new_options_definition.settings)
|
525
512
|
]
|
526
513
|
end
|
527
514
|
end
|
@@ -539,7 +526,7 @@ module Generators
|
|
539
526
|
col_spec = ::DeclareSchema::Model::Column.new(model, current_table_name, column)
|
540
527
|
schema_attributes = col_spec.schema_attributes
|
541
528
|
type = schema_attributes.delete(:type) or raise "no :type found in #{schema_attributes.inspect}"
|
542
|
-
[
|
529
|
+
[type, schema_attributes]
|
543
530
|
end
|
544
531
|
end
|
545
532
|
|
@@ -549,7 +536,7 @@ module Generators
|
|
549
536
|
col_spec = ::DeclareSchema::Model::Column.new(model, current_table_name, column)
|
550
537
|
schema_attributes = col_spec.schema_attributes
|
551
538
|
type = schema_attributes.delete(:type) or raise "no :type found in #{schema_attributes.inspect}"
|
552
|
-
[
|
539
|
+
[type, schema_attributes]
|
553
540
|
end
|
554
541
|
end
|
555
542
|
|
@@ -584,8 +571,8 @@ module Generators
|
|
584
571
|
# TODO: rewrite this method to use charset and collation variables rather than manipulating strings. -Colin
|
585
572
|
def fix_mysql_charset_and_collation(dumped_schema)
|
586
573
|
if !dumped_schema['options: ']
|
587
|
-
dumped_schema.sub!('",', "\", options: \"DEFAULT CHARSET=#{
|
588
|
-
"COLLATE=#{
|
574
|
+
dumped_schema.sub!('",', "\", options: \"DEFAULT CHARSET=#{::DeclareSchema.default_charset} "+
|
575
|
+
"COLLATE=#{::DeclareSchema.default_collation}\",")
|
589
576
|
end
|
590
577
|
default_charset = dumped_schema[/CHARSET=(\w+)/, 1] or raise "unable to find charset in #{dumped_schema.inspect}"
|
591
578
|
default_collation = dumped_schema[/COLLATE=(\w+)/, 1] || default_collation_from_charset(default_charset) or
|