declare_schema 0.8.0.pre.6 → 0.10.1

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.
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