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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/declare_schema_build.yml +1 -1
  3. data/CHANGELOG.md +28 -1
  4. data/Gemfile.lock +1 -1
  5. data/README.md +91 -13
  6. data/lib/declare_schema.rb +46 -0
  7. data/lib/declare_schema/dsl.rb +39 -0
  8. data/lib/declare_schema/extensions/active_record/fields_declaration.rb +23 -4
  9. data/lib/declare_schema/model.rb +51 -59
  10. data/lib/declare_schema/model/field_spec.rb +11 -8
  11. data/lib/declare_schema/model/foreign_key_definition.rb +4 -8
  12. data/lib/declare_schema/model/habtm_model_shim.rb +1 -1
  13. data/lib/declare_schema/model/index_definition.rb +0 -19
  14. data/lib/declare_schema/schema_change/all.rb +22 -0
  15. data/lib/declare_schema/schema_change/base.rb +45 -0
  16. data/lib/declare_schema/schema_change/column_add.rb +27 -0
  17. data/lib/declare_schema/schema_change/column_change.rb +32 -0
  18. data/lib/declare_schema/schema_change/column_remove.rb +20 -0
  19. data/lib/declare_schema/schema_change/column_rename.rb +23 -0
  20. data/lib/declare_schema/schema_change/foreign_key_add.rb +25 -0
  21. data/lib/declare_schema/schema_change/foreign_key_remove.rb +20 -0
  22. data/lib/declare_schema/schema_change/index_add.rb +33 -0
  23. data/lib/declare_schema/schema_change/index_remove.rb +20 -0
  24. data/lib/declare_schema/schema_change/primary_key_change.rb +33 -0
  25. data/lib/declare_schema/schema_change/table_add.rb +37 -0
  26. data/lib/declare_schema/schema_change/table_change.rb +36 -0
  27. data/lib/declare_schema/schema_change/table_remove.rb +22 -0
  28. data/lib/declare_schema/schema_change/table_rename.rb +22 -0
  29. data/lib/declare_schema/version.rb +1 -1
  30. data/lib/generators/declare_schema/migration/migrator.rb +189 -202
  31. data/lib/generators/declare_schema/support/model.rb +4 -4
  32. data/spec/lib/declare_schema/api_spec.rb +7 -7
  33. data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +41 -15
  34. data/spec/lib/declare_schema/field_spec_spec.rb +50 -6
  35. data/spec/lib/declare_schema/generator_spec.rb +3 -3
  36. data/spec/lib/declare_schema/interactive_primary_key_spec.rb +116 -26
  37. data/spec/lib/declare_schema/migration_generator_spec.rb +1891 -845
  38. data/spec/lib/declare_schema/model/column_spec.rb +47 -17
  39. data/spec/lib/declare_schema/model/foreign_key_definition_spec.rb +134 -57
  40. data/spec/lib/declare_schema/model/habtm_model_shim_spec.rb +3 -3
  41. data/spec/lib/declare_schema/model/index_definition_spec.rb +188 -77
  42. data/spec/lib/declare_schema/model/table_options_definition_spec.rb +75 -11
  43. data/spec/lib/declare_schema/schema_change/base_spec.rb +75 -0
  44. data/spec/lib/declare_schema/schema_change/column_add_spec.rb +30 -0
  45. data/spec/lib/declare_schema/schema_change/column_change_spec.rb +33 -0
  46. data/spec/lib/declare_schema/schema_change/column_remove_spec.rb +30 -0
  47. data/spec/lib/declare_schema/schema_change/column_rename_spec.rb +28 -0
  48. data/spec/lib/declare_schema/schema_change/foreign_key_add_spec.rb +29 -0
  49. data/spec/lib/declare_schema/schema_change/foreign_key_remove_spec.rb +29 -0
  50. data/spec/lib/declare_schema/schema_change/index_add_spec.rb +56 -0
  51. data/spec/lib/declare_schema/schema_change/index_remove_spec.rb +29 -0
  52. data/spec/lib/declare_schema/schema_change/primary_key_change_spec.rb +69 -0
  53. data/spec/lib/declare_schema/schema_change/table_add_spec.rb +50 -0
  54. data/spec/lib/declare_schema/schema_change/table_change_spec.rb +30 -0
  55. data/spec/lib/declare_schema/schema_change/table_remove_spec.rb +27 -0
  56. data/spec/lib/declare_schema/schema_change/table_rename_spec.rb +27 -0
  57. data/spec/lib/declare_schema_spec.rb +101 -0
  58. data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +71 -13
  59. data/spec/support/acceptance_spec_helpers.rb +2 -2
  60. metadata +33 -3
  61. 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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeclareSchema
4
- VERSION = "0.8.0.pre.6"
4
+ VERSION = "0.10.1"
5
5
  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, :disable_indexing, :disable_constraints
24
- attr_reader :active_record_class, :default_charset, :default_collation, :before_generating_migration_callback
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
- g = Migrator.new
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 = nil
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.each_pair do |old_name, new_name|
129
- new_name = new_name[:table_name] if new_name.is_a?(Hash)
130
- next unless new_name
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&.[](table_name.to_sym))
152
- # A hash of table renames has been provided
153
-
154
- column_renames.each_pair do |old_name, new_name|
155
- if to_add.delete(new_name.to_s) && to_remove.delete(old_name.to_s)
156
- to_rename[old_name.to_s] = new_name.to_s
157
- else
158
- raise Error, "Invalid rename specified: #{old_name} => #{new_name}"
159
- end
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
- "rename_table :#{old_name}, :#{new_name}"
217
- end * "\n"
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
- "drop_table :#{t}"
224
- end * "\n"
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
- create_table(models_by_table_name[t])
231
- end * "\n\n"
232
- undo_creates = to_create.map do |t|
233
- "drop_table :#{t}"
234
- end * "\n"
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, undo, index_change, undo_index, fk_change, undo_fk, table_options_change, undo_table_options_change = change_table(model, table)
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
- up = [renames, drops, creates, changes, index_changes, fk_changes, table_options_changes].flatten.reject(&:blank?) * "\n\n"
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
- [up, down]
263
+ ordered_migration_commands = order_migrations(migration_commands)
264
+
265
+ up_and_down_migrations(ordered_migration_commands)
265
266
  end
266
267
 
267
- def create_table(model)
268
- longest_field_name = model.field_specs.values.map { |f| f.type.to_s.length }.max
269
- disable_auto_increment = model.respond_to?(:disable_auto_increment) && model.disable_auto_increment
270
- table_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, table_options_for_model(model))
271
- field_definitions = [
272
- ("t.integer :id, limit: 8, auto_increment: false, primary_key: true" if disable_auto_increment),
273
- *(model.field_specs.values.sort_by(&:position).map { |f| create_field(f, longest_field_name) })
274
- ].compact
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
- <<~EOS.strip
277
- create_table :#{model.table_name}, #{create_table_options(model, disable_auto_increment)} do |t|
278
- #{field_definitions.join("\n")}
279
- end
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
- #{table_options_definition.alter_table_statement unless ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)}
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
- if model.primary_key.blank? || disable_auto_increment
289
- "id: false"
290
- elsif model.primary_key == "id"
291
- "id: :bigint"
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
- "primary_key: :#{model.primary_key}"
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] || Migrator.default_charset,
303
- collation: model.table_options[:collation] || Migrator.default_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 { |i| i.to_add_statement(model.table_name) }
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 { |fk| fk.to_add_statement }
314
- end
315
-
316
- def create_field(field_spec, field_name_width)
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.primary_key].nil? && model.primary_key.present?
327
- if model.primary_key.present?
328
- db_columns.delete(model.primary_key)
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.primary_key] if key_missing && model.primary_key.present?
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.primary_key.to_sym] if model.primary_key.present?
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
- "rename_column :#{new_table_name}, :#{old_name}, :#{new_name}"
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 = to_add.sort_by { |c| model.field_specs[c]&.position || 0 }
362
+ to_add.sort_by! { |c| model.field_specs[c]&.position || 0 }
363
+
353
364
  adds = to_add.map do |c|
354
- args =
365
+ type, options =
355
366
  if (spec = model.field_specs[c])
356
- options = spec.sql_options.merge(fk_field_options(model, c))
357
- [":#{spec.type}", *format_options(options.compact)]
367
+ [spec.type, spec.sql_options.merge(fk_field_options(model, c)).compact]
358
368
  else
359
- [":integer"]
369
+ [:integer, {}]
360
370
  end
361
- ["add_column :#{new_table_name}, :#{c}", *args].join(', ')
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
- "remove_column :#{new_table_name}, :#{c}"
369
- end
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
- changes << ["change_column #{new_table_name.to_sym.inspect}", col_name_to_change.to_sym.inspect,
389
- type.to_sym.inspect, *format_options(normalized_schema_attrs)].join(", ")
390
- undo_changes << change_column_back(model, current_table_name, orig_col_name)
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, undo_index_changes = change_indexes(model, current_table_name, to_remove)
395
- fk_changes, undo_fk_changes = if ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
396
- [[], []]
397
- else
398
- change_foreign_key_constraints(model, current_table_name)
399
- end
400
- table_options_changes, undo_table_options_changes = if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
401
- change_table_options(model, current_table_name)
402
- else
403
- [[], []]
404
- end
405
-
406
- [(renames + adds + removes + changes) * "\n",
407
- (undo_renames + undo_adds + undo_removes + undo_changes) * "\n",
408
- index_changes * "\n",
409
- undo_index_changes * "\n",
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, to_remove)
417
- Migrator.disable_constraints and return [[], []]
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
- existing_has_primary_key = existing_indexes.any? do |i|
430
- i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME &&
431
- !i.fields.all? { |f| to_remove.include?(f) } # if we're removing the primary key column(s), the primary key index will be removed too
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
- undo_add_indexes = []
436
- add_indexes = (model_indexes - existing_indexes).map do |i|
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
- # the order is important here - adding a :unique, for instance needs to remove then add
447
- [drop_indexes + add_indexes, undo_add_indexes + undo_drop_indexes]
448
- end
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
- def drop_index(table, name)
451
- # see https://hobo.lighthouseapp.com/projects/8324/tickets/566
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
- Migrator.disable_indexing and return [[], []]
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
- undo_drop_fks << fk.to_add_statement
474
- remove_foreign_key(new_table_name, fk.constraint_name)
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
- [drop_fks + add_fks, undo_add_fks + undo_drop_fks]
478
- end
479
-
480
- def remove_foreign_key(old_table_name, fk_name)
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
- [new_options_definition.alter_table_statement],
524
- [old_options_definition.alter_table_statement]
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
- ["add_column :#{current_table_name}, :#{col_name}, #{type.inspect}", *format_options(schema_attributes)].join(', ')
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
- ["change_column #{current_table_name.to_sym.inspect}", col_name.to_sym.inspect, type.to_sym.inspect, *format_options(schema_attributes)].join(', ')
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=#{Generators::DeclareSchema::Migration::Migrator.default_charset} "+
588
- "COLLATE=#{Generators::DeclareSchema::Migration::Migrator.default_collation}\",")
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