declare_schema 0.10.0 → 0.12.0.pre.2

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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -1
  3. data/Gemfile.lock +1 -1
  4. data/README.md +16 -6
  5. data/lib/declare_schema.rb +12 -1
  6. data/lib/declare_schema/extensions/active_record/fields_declaration.rb +4 -2
  7. data/lib/declare_schema/model.rb +59 -15
  8. data/lib/declare_schema/model/column.rb +2 -2
  9. data/lib/declare_schema/model/field_spec.rb +4 -4
  10. data/lib/declare_schema/model/foreign_key_definition.rb +6 -11
  11. data/lib/declare_schema/model/habtm_model_shim.rb +2 -2
  12. data/lib/declare_schema/model/index_definition.rb +8 -25
  13. data/lib/declare_schema/schema_change/all.rb +22 -0
  14. data/lib/declare_schema/schema_change/base.rb +45 -0
  15. data/lib/declare_schema/schema_change/column_add.rb +27 -0
  16. data/lib/declare_schema/schema_change/column_change.rb +32 -0
  17. data/lib/declare_schema/schema_change/column_remove.rb +20 -0
  18. data/lib/declare_schema/schema_change/column_rename.rb +23 -0
  19. data/lib/declare_schema/schema_change/foreign_key_add.rb +25 -0
  20. data/lib/declare_schema/schema_change/foreign_key_remove.rb +20 -0
  21. data/lib/declare_schema/schema_change/index_add.rb +33 -0
  22. data/lib/declare_schema/schema_change/index_remove.rb +20 -0
  23. data/lib/declare_schema/schema_change/primary_key_change.rb +33 -0
  24. data/lib/declare_schema/schema_change/table_add.rb +37 -0
  25. data/lib/declare_schema/schema_change/table_change.rb +36 -0
  26. data/lib/declare_schema/schema_change/table_remove.rb +22 -0
  27. data/lib/declare_schema/schema_change/table_rename.rb +22 -0
  28. data/lib/declare_schema/version.rb +1 -1
  29. data/lib/generators/declare_schema/migration/USAGE +14 -24
  30. data/lib/generators/declare_schema/migration/migration_generator.rb +40 -38
  31. data/lib/generators/declare_schema/migration/migrator.rb +190 -187
  32. data/lib/generators/declare_schema/migration/templates/migration.rb.erb +3 -3
  33. data/spec/lib/declare_schema/api_spec.rb +3 -1
  34. data/spec/lib/declare_schema/field_spec_spec.rb +3 -3
  35. data/spec/lib/declare_schema/generator_spec.rb +2 -2
  36. data/spec/lib/declare_schema/interactive_primary_key_spec.rb +60 -25
  37. data/spec/lib/declare_schema/migration_generator_spec.rb +471 -377
  38. data/spec/lib/declare_schema/model/column_spec.rb +2 -6
  39. data/spec/lib/declare_schema/model/foreign_key_definition_spec.rb +28 -16
  40. data/spec/lib/declare_schema/model/habtm_model_shim_spec.rb +4 -6
  41. data/spec/lib/declare_schema/model/index_definition_spec.rb +4 -4
  42. data/spec/lib/declare_schema/schema_change/base_spec.rb +75 -0
  43. data/spec/lib/declare_schema/schema_change/column_add_spec.rb +30 -0
  44. data/spec/lib/declare_schema/schema_change/column_change_spec.rb +33 -0
  45. data/spec/lib/declare_schema/schema_change/column_remove_spec.rb +30 -0
  46. data/spec/lib/declare_schema/schema_change/column_rename_spec.rb +28 -0
  47. data/spec/lib/declare_schema/schema_change/foreign_key_add_spec.rb +29 -0
  48. data/spec/lib/declare_schema/schema_change/foreign_key_remove_spec.rb +29 -0
  49. data/spec/lib/declare_schema/schema_change/index_add_spec.rb +56 -0
  50. data/spec/lib/declare_schema/schema_change/index_remove_spec.rb +29 -0
  51. data/spec/lib/declare_schema/schema_change/primary_key_change_spec.rb +69 -0
  52. data/spec/lib/declare_schema/schema_change/table_add_spec.rb +50 -0
  53. data/spec/lib/declare_schema/schema_change/table_change_spec.rb +30 -0
  54. data/spec/lib/declare_schema/schema_change/table_remove_spec.rb +27 -0
  55. data/spec/lib/declare_schema/schema_change/table_rename_spec.rb +27 -0
  56. data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +59 -11
  57. data/spec/spec_helper.rb +1 -1
  58. data/spec/support/acceptance_spec_helpers.rb +2 -2
  59. metadata +34 -5
@@ -1,47 +1,37 @@
1
1
  Description:
2
2
 
3
3
  This generator compares your existing schema against the
4
- schema declared inside your fields declarations in your
5
- models.
4
+ schema declared inside your declare_schema do ... end
5
+ declarations in your models.
6
6
 
7
7
  If the generator finds differences, it will display the
8
- migration it has created, and ask you if you wish to
9
- [g]enerate migration, generate and [m]igrate now or [c]ancel?
10
- Enter "g" to just generate the migration but do not run it.
11
- Enter "m" to generate the migration and run it, or press "c"
12
- to do nothing.
13
-
14
- The generator will then prompt you for the migration name,
8
+ migration it has created, and prompt you for the migration name,
15
9
  supplying a numbered default name.
16
10
 
17
11
  The generator is conservative and will prompt you to resolve
18
12
  any ambiguities.
19
13
 
20
- Examples:
14
+ Example:
21
15
 
22
- $ rails generate declare_schema:migration
16
+ $ bundle exec rails generate declare_schema:migration
23
17
 
24
18
  ---------- Up Migration ----------
25
- create_table :foos do |t|
19
+ create_table :users do |t|
20
+ t.string :first_name, limit: 50
21
+ t.string :last_name, limit: 50
26
22
  t.datetime :created_at
27
23
  t.datetime :updated_at
28
24
  end
29
25
  ----------------------------------
30
26
 
31
27
  ---------- Down Migration --------
32
- drop_table :foos
28
+ drop_table :users
33
29
  ----------------------------------
34
- What now: [g]enerate migration, generate and [m]igrate now or [c]ancel? m
35
-
36
- Migration filename:
37
- (you can type spaces instead of '_' -- every little helps)
38
- Filename [declare_schema_migration_2]: create_foo
30
+ Migration filename: (spaces will be converted to _) [declare_schema_migration_2]: create users
39
31
  exists db/migrate
40
- create db/migrate/20091023183838_create_foo.rb
41
- (in /work/foo)
42
- == CreateFoo: migrating ======================================================
43
- -- create_table(:yos)
44
- -> 0.0856s
45
- == CreateFoo: migrated (0.0858s) =============================================
32
+ create db/migrate/20091023183838_create_users.rb
33
+
34
+ Not running migration since --migrate not given. When you are ready, run:
46
35
 
36
+ bundle exec rails db:migrate
47
37
 
@@ -38,20 +38,18 @@ module DeclareSchema
38
38
  type: :boolean,
39
39
  desc: "Don't prompt for a migration name - just pick one"
40
40
 
41
- class_option :generate,
42
- aliases: '-g',
43
- type: :boolean,
44
- desc: "Don't prompt for action - generate the migration"
45
-
46
41
  class_option :migrate,
47
42
  aliases: '-m',
48
43
  type: :boolean,
49
- desc: "Don't prompt for action - generate and migrate"
44
+ desc: "After generating migration, run it"
50
45
 
51
46
  def migrate
52
47
  return if migrations_pending?
53
48
 
54
- generator = Generators::DeclareSchema::Migration::Migrator.new(->(c, d, k, p) { extract_renames!(c, d, k, p) })
49
+ generator = Generators::DeclareSchema::Migration::Migrator.new do |to_create, to_drop, kind_str, name_prefix|
50
+ extract_renames!(to_create, to_drop, kind_str, name_prefix)
51
+ end
52
+
55
53
  up, down = generator.generate
56
54
 
57
55
  if up.blank?
@@ -67,34 +65,31 @@ module DeclareSchema
67
65
  say down
68
66
  say "----------------------------------"
69
67
 
70
- action = options[:generate] && 'g' ||
71
- options[:migrate] && 'm' ||
72
- choose("\nWhat now: [g]enerate migration, generate and [m]igrate now or [c]ancel?", /^(g|m|c)$/)
73
-
74
- if action != 'c'
75
- if name.blank? && !options[:default_name]
76
- final_migration_name = choose("\nMigration filename: [<enter>=#{migration_name}|<custom_name>]:", /^[a-z0-9_ ]*$/, migration_name).strip.gsub(' ', '_')
77
- end
78
- final_migration_name = migration_name if final_migration_name.blank?
79
-
80
- up.gsub!("\n", "\n ")
81
- up.gsub!(/ +\n/, "\n")
82
- down.gsub!("\n", "\n ")
83
- down.gsub!(/ +\n/, "\n")
84
-
85
- @up = up
86
- @down = down
87
- @migration_class_name = final_migration_name.camelize
88
-
89
- migration_template('migration.rb.erb', "db/migrate/#{final_migration_name.underscore}.rb")
90
- if action == 'm'
91
- case Rails::VERSION::MAJOR
92
- when 4
93
- rake('db:migrate')
94
- else
95
- rails_command('db:migrate')
96
- end
68
+ final_migration_name =
69
+ name.presence ||
70
+ if !options[:default_name]
71
+ choose("\nMigration filename (spaces will be converted to _) [#{default_migration_name}]:", /^[a-z0-9_ ]*$/,
72
+ default_migration_name).strip.gsub(' ', '_').presence
73
+ end ||
74
+ default_migration_name
75
+
76
+ @up = indent(up.strip, 4)
77
+ @down = indent(down.strip, 4)
78
+ @migration_class_name = final_migration_name.camelize
79
+
80
+ migration_template('migration.rb.erb', "db/migrate/#{final_migration_name.underscore}.rb")
81
+
82
+ db_migrate_command = ::DeclareSchema.db_migrate_command
83
+ if options[:migrate]
84
+ say db_migrate_command
85
+ bare_rails_command = db_migrate_command.sub(/\Abundle exec +/, '').sub(/\Arake +|rails +/, '')
86
+ if ActiveSupport::VERSION::MAJOR < 5
87
+ rake(bare_rails_command)
88
+ else
89
+ rails_command(bare_rails_command)
97
90
  end
91
+ else
92
+ say "\nNot running migration since --migrate not given. When you are ready, run:\n\n #{db_migrate_command}\n\n"
98
93
  end
99
94
  rescue ::DeclareSchema::UnknownTypeError => ex
100
95
  say "Invalid field type: #{ex}"
@@ -102,8 +97,15 @@ module DeclareSchema
102
97
 
103
98
  private
104
99
 
100
+ if ActiveSupport::VERSION::MAJOR < 5
101
+ def indent(string, columns)
102
+ whitespace = ' ' * columns
103
+ string.gsub("\n", "\n#{whitespace}").gsub(/ +\n/, "\n")
104
+ end
105
+ end
106
+
105
107
  def migrations_pending?
106
- migrations = case Rails::VERSION::MAJOR
108
+ migrations = case ActiveSupport::VERSION::MAJOR
107
109
  when 4
108
110
  ActiveRecord::Migrator.migrations('db/migrate')
109
111
  when 5
@@ -111,7 +113,7 @@ module DeclareSchema
111
113
  else
112
114
  ActiveRecord::MigrationContext.new(ActiveRecord::Migrator.migrations_paths, ActiveRecord::SchemaMigration).migrations
113
115
  end
114
- pending_migrations = case Rails::VERSION::MAJOR
116
+ pending_migrations = case ActiveSupport::VERSION::MAJOR
115
117
  when 4, 5
116
118
  ActiveRecord::Migrator.new(:up, migrations).pending_migrations
117
119
  else
@@ -176,8 +178,8 @@ module DeclareSchema
176
178
  to_rename
177
179
  end
178
180
 
179
- def migration_name
180
- name || Generators::DeclareSchema::Migration::Migrator.default_migration_name
181
+ def default_migration_name
182
+ Generators::DeclareSchema::Migration::Migrator.default_migration_name
181
183
  end
182
184
  end
183
185
  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
@@ -24,9 +25,7 @@ module Generators
24
25
  end
25
26
 
26
27
  def run(renames = {})
27
- g = Migrator.new
28
- g.renames = renames
29
- g.generate
28
+ Migrator.new(renames: renames).generate
30
29
  end
31
30
 
32
31
  def default_migration_name
@@ -48,14 +47,12 @@ module Generators
48
47
  deprecate :default_charset=, :default_collation=, :default_charset, :default_collation, deprecator: ActiveSupport::Deprecation.new('1.0', 'declare_schema')
49
48
  end
50
49
 
51
- def initialize(ambiguity_resolver = {})
52
- @ambiguity_resolver = ambiguity_resolver
50
+ def initialize(renames: nil, &block)
51
+ @ambiguity_resolver = block
53
52
  @drops = []
54
- @renames = nil
53
+ @renames = renames
55
54
  end
56
55
 
57
- attr_accessor :renames
58
-
59
56
  def load_rails_models
60
57
  ActiveRecord::Migration.verbose = false
61
58
 
@@ -109,19 +106,24 @@ module Generators
109
106
  # return a hash of table renames and modifies the passed arrays so
110
107
  # that renamed tables are no longer listed as to_create or to_drop
111
108
  def extract_table_renames!(to_create, to_drop)
112
- if renames
109
+ if @renames
113
110
  # A hash of table renames has been provided
114
111
 
115
112
  to_rename = {}
116
- renames.each_pair do |old_name, new_name|
117
- new_name = new_name[:table_name] if new_name.is_a?(Hash)
118
- next unless new_name
119
-
120
- if to_create.delete(new_name.to_s) && to_drop.delete(old_name.to_s)
121
- to_rename[old_name.to_s] = new_name.to_s
122
- else
123
- 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]
124
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
125
127
  end
126
128
  to_rename
127
129
 
@@ -133,18 +135,22 @@ module Generators
133
135
  end
134
136
  end
135
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
136
140
  def extract_column_renames!(to_add, to_remove, table_name)
137
- if renames
141
+ if @renames
138
142
  to_rename = {}
139
- if (column_renames = renames&.[](table_name.to_sym))
140
- # A hash of table renames has been provided
141
-
142
- column_renames.each_pair do |old_name, new_name|
143
- if to_add.delete(new_name.to_s) && to_remove.delete(old_name.to_s)
144
- to_rename[old_name.to_s] = new_name.to_s
145
- else
146
- raise Error, "Invalid rename specified: #{old_name} => #{new_name}"
147
- 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
148
154
  end
149
155
  end
150
156
  to_rename
@@ -181,6 +187,12 @@ module Generators
181
187
  models, db_tables = models_and_tables
182
188
  models_by_table_name = {}
183
189
  models.each do |m|
190
+ m.try(:field_specs)&.each do |_name, field_spec|
191
+ if (pre_migration = field_spec.options.delete(:pre_migration))
192
+ pre_migration.call(field_spec)
193
+ end
194
+ end
195
+
184
196
  if !models_by_table_name.has_key?(m.table_name)
185
197
  models_by_table_name[m.table_name] = m
186
198
  elsif m.superclass == models_by_table_name[m.table_name].superclass.superclass
@@ -197,90 +209,108 @@ module Generators
197
209
 
198
210
  to_create = model_table_names - db_tables
199
211
  to_drop = db_tables - model_table_names - self.class.always_ignore_tables
200
- to_change = model_table_names
201
212
  to_rename = extract_table_renames!(to_create, to_drop)
213
+ to_change = model_table_names
202
214
 
203
215
  renames = to_rename.map do |old_name, new_name|
204
- "rename_table :#{old_name}, :#{new_name}"
205
- end * "\n"
206
- undo_renames = to_rename.map do |old_name, new_name|
207
- "rename_table :#{new_name}, :#{old_name}"
208
- end * "\n"
216
+ ::DeclareSchema::SchemaChange::TableRename.new(old_name, new_name)
217
+ end
209
218
 
210
219
  drops = to_drop.map do |t|
211
- "drop_table :#{t}"
212
- end * "\n"
213
- undo_drops = to_drop.map do |t|
214
- add_table_back(t)
215
- end * "\n\n"
220
+ ::DeclareSchema::SchemaChange::TableRemove.new(t, add_table_back(t))
221
+ end
216
222
 
217
223
  creates = to_create.map do |t|
218
- create_table(models_by_table_name[t])
219
- end * "\n\n"
220
- undo_creates = to_create.map do |t|
221
- "drop_table :#{t}"
222
- end * "\n"
224
+ model = models_by_table_name[t]
225
+ disable_auto_increment = model.try(:disable_auto_increment)
226
+
227
+ primary_key_definition =
228
+ if disable_auto_increment
229
+ [:integer, :id, limit: 8, auto_increment: false, primary_key: true]
230
+ end
231
+
232
+ field_definitions = model.field_specs.values.sort_by(&:position).map do |f|
233
+ [f.type, f.name, f.sql_options]
234
+ end
235
+
236
+ table_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, table_options_for_model(model))
237
+ table_options = create_table_options(model, disable_auto_increment)
238
+
239
+ table_add = ::DeclareSchema::SchemaChange::TableAdd.new(t,
240
+ Array(primary_key_definition) + field_definitions,
241
+ table_options,
242
+ sql_options: table_options_definition.settings)
243
+ [
244
+ table_add,
245
+ *Array((create_indexes(model) if ::DeclareSchema.default_generate_indexing)),
246
+ *Array((create_constraints(model) if ::DeclareSchema.default_generate_foreign_keys))
247
+ ]
248
+ end
223
249
 
224
250
  changes = []
225
- undo_changes = []
226
251
  index_changes = []
227
- undo_index_changes = []
228
252
  fk_changes = []
229
- undo_fk_changes = []
230
253
  table_options_changes = []
231
- undo_table_options_changes = []
232
254
 
233
255
  to_change.each do |t|
234
256
  model = models_by_table_name[t]
235
257
  table = to_rename.key(t) || model.table_name
236
258
  if table.in?(db_tables)
237
- change, undo, index_change, undo_index, fk_change, undo_fk, table_options_change, undo_table_options_change = change_table(model, table)
259
+ change, index_change, fk_change, table_options_change = change_table(model, table)
238
260
  changes << change
239
- undo_changes << undo
240
261
  index_changes << index_change
241
- undo_index_changes << undo_index
242
262
  fk_changes << fk_change
243
- undo_fk_changes << undo_fk
244
263
  table_options_changes << table_options_change
245
- undo_table_options_changes << undo_table_options_change
246
264
  end
247
265
  end
248
266
 
249
- up = [renames, drops, creates, changes, index_changes, fk_changes, table_options_changes].flatten.reject(&:blank?) * "\n\n"
250
- down = [undo_changes, undo_renames, undo_drops, undo_creates, undo_index_changes, undo_fk_changes, undo_table_options_changes].flatten.reject(&:blank?) * "\n\n"
267
+ migration_commands = [renames, drops, creates, changes, index_changes, fk_changes, table_options_changes].flatten
268
+
269
+ ordered_migration_commands = order_migrations(migration_commands)
251
270
 
252
- [up, down]
271
+ up_and_down_migrations(ordered_migration_commands)
253
272
  end
254
273
 
255
- def create_table(model)
256
- longest_field_name = model.field_specs.values.map { |f| f.type.to_s.length }.max
257
- disable_auto_increment = model.respond_to?(:disable_auto_increment) && model.disable_auto_increment
258
- table_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, table_options_for_model(model))
259
- field_definitions = [
260
- ("t.integer :id, limit: 8, auto_increment: false, primary_key: true" if disable_auto_increment),
261
- *(model.field_specs.values.sort_by(&:position).map { |f| create_field(f, longest_field_name) })
262
- ].compact
274
+ MIGRATION_ORDER = %w[ TableRename
275
+ TableAdd
276
+ TableChange
277
+ ColumnAdd
278
+ ColumnRename
279
+ ColumnChange
280
+ PrimaryKeyChange
281
+ IndexAdd
282
+ ForeignKeyAdd
283
+ ForeignKeyRemove
284
+ IndexRemove
285
+ ColumnRemove
286
+ TableRemove ]
263
287
 
264
- <<~EOS.strip
265
- create_table :#{model.table_name}, #{create_table_options(model, disable_auto_increment)} do |t|
266
- #{field_definitions.join("\n")}
267
- end
288
+ def order_migrations(migration_commands)
289
+ migration_commands.each_with_index.sort_by do |command, index|
290
+ command_type = command.class.name.gsub(/.*::/, '')
291
+ priority = MIGRATION_ORDER.index(command_type) or raise "#{command_type.inspect} not found in #{MIGRATION_ORDER.inspect}"
292
+ [priority, index] # index keeps the sort stable in case of a tie
293
+ end.map(&:first) # remove the index
294
+ end
295
+
296
+ private
268
297
 
269
- #{table_options_definition.alter_table_statement unless ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)}
270
- #{create_indexes(model).join("\n") if ::DeclareSchema.default_generate_indexing}
271
- #{create_constraints(model).join("\n") if ::DeclareSchema.default_generate_foreign_keys}
272
- EOS
298
+ def up_and_down_migrations(migration_commands)
299
+ up = migration_commands.map(&:up ).select(&:present?)
300
+ down = migration_commands.map(&:down).select(&:present?).reverse
301
+
302
+ [up * "\n\n", down * "\n\n"]
273
303
  end
274
304
 
275
305
  def create_table_options(model, disable_auto_increment)
276
- primary_key = model._defined_primary_key
306
+ primary_key = model._declared_primary_key
277
307
  if primary_key.blank? || disable_auto_increment
278
- "id: false"
308
+ { id: false }
279
309
  elsif primary_key == "id"
280
- "id: :bigint"
310
+ { id: :bigint }
281
311
  else
282
- "primary_key: :#{primary_key}"
283
- end
312
+ { primary_key: primary_key.to_sym }
313
+ end.merge(model._table_options)
284
314
  end
285
315
 
286
316
  def table_options_for_model(model)
@@ -288,42 +318,41 @@ module Generators
288
318
  {}
289
319
  else
290
320
  {
291
- charset: model.table_options[:charset] || ::DeclareSchema.default_charset,
292
- collation: model.table_options[:collation] || ::DeclareSchema.default_collation
321
+ charset: model._table_options&.[](:charset) || ::DeclareSchema.default_charset,
322
+ collation: model._table_options&.[](:collation) || ::DeclareSchema.default_collation
293
323
  }
294
324
  end
295
325
  end
296
326
 
327
+ # TODO: TECH-5338: optimize that index doesn't need to be dropped on undo since entire table will be dropped
297
328
  def create_indexes(model)
298
- model.index_definitions.map { |i| i.to_add_statement(model.table_name) }
329
+ model.index_definitions.map do |i|
330
+ ::DeclareSchema::SchemaChange::IndexAdd.new(model.table_name, i.columns, unique: i.unique, where: i.where, name: i.name)
331
+ end
299
332
  end
300
333
 
301
334
  def create_constraints(model)
302
- model.constraint_specs.map { |fk| fk.to_add_statement }
303
- end
304
-
305
- def create_field(field_spec, field_name_width)
306
- options = field_spec.sql_options.merge(fk_field_options(field_spec.model, field_spec.name))
307
- args = [field_spec.name.inspect] + format_options(options.compact)
308
- format("t.%-*s %s", field_name_width, field_spec.type, args.join(', '))
335
+ model.constraint_specs.map do |fk|
336
+ ::DeclareSchema::SchemaChange::ForeignKeyAdd.new(fk.child_table_name, fk.parent_table_name,
337
+ column_name: fk.foreign_key_name, name: fk.constraint_name)
338
+ end
309
339
  end
310
340
 
311
341
  def change_table(model, current_table_name)
312
342
  new_table_name = model.table_name
313
343
 
314
344
  db_columns = model.connection.columns(current_table_name).index_by(&:name)
315
- key_missing = db_columns[model._defined_primary_key].nil? && model._defined_primary_key.present?
316
- if model._defined_primary_key.present?
317
- db_columns.delete(model._defined_primary_key)
345
+ if (pk = model._declared_primary_key.presence)
346
+ key_was_in_db_columns = db_columns.delete(pk)
318
347
  end
319
348
 
320
349
  model_column_names = model.field_specs.keys.map(&:to_s)
321
350
  db_column_names = db_columns.keys.map(&:to_s)
322
351
 
323
352
  to_add = model_column_names - db_column_names
324
- to_add += [model._defined_primary_key] if key_missing && model._defined_primary_key.present?
353
+ to_add << pk if pk && !key_was_in_db_columns
325
354
  to_remove = db_column_names - model_column_names
326
- to_remove -= [model._defined_primary_key.to_sym] if model._defined_primary_key.present?
355
+ to_remove -= [pk.to_sym] if pk # TODO: The .to_sym here means this code is always a no-op, right? -Colin
327
356
 
328
357
  to_rename = extract_column_renames!(to_add, to_remove, new_table_name)
329
358
 
@@ -332,37 +361,28 @@ module Generators
332
361
  to_change = db_column_names & model_column_names
333
362
 
334
363
  renames = to_rename.map do |old_name, new_name|
335
- "rename_column :#{new_table_name}, :#{old_name}, :#{new_name}"
336
- end
337
- undo_renames = to_rename.map do |old_name, new_name|
338
- "rename_column :#{new_table_name}, :#{new_name}, :#{old_name}"
364
+ ::DeclareSchema::SchemaChange::ColumnRename.new(new_table_name, old_name, new_name)
339
365
  end
340
366
 
341
- to_add = to_add.sort_by { |c| model.field_specs[c]&.position || 0 }
367
+ to_add.sort_by! { |c| model.field_specs[c]&.position || 0 }
368
+
342
369
  adds = to_add.map do |c|
343
- args =
370
+ type, options =
344
371
  if (spec = model.field_specs[c])
345
- options = spec.sql_options.merge(fk_field_options(model, c))
346
- [":#{spec.type}", *format_options(options.compact)]
372
+ [spec.type, spec.sql_options.merge(fk_field_options(model, c)).compact]
347
373
  else
348
- [":integer"]
374
+ [:integer, {}]
349
375
  end
350
- ["add_column :#{new_table_name}, :#{c}", *args].join(', ')
351
- end
352
- undo_adds = to_add.map do |c|
353
- "remove_column :#{new_table_name}, :#{c}"
376
+ ::DeclareSchema::SchemaChange::ColumnAdd.new(new_table_name, c, type, options)
354
377
  end
355
378
 
356
379
  removes = to_remove.map do |c|
357
- "remove_column :#{new_table_name}, :#{c}"
358
- end
359
- undo_removes = to_remove.map do |c|
360
- add_column_back(model, current_table_name, c)
380
+ old_type, old_options = add_column_back(model, current_table_name, c)
381
+ ::DeclareSchema::SchemaChange::ColumnRemove.new(new_table_name, c, old_type, old_options)
361
382
  end
362
383
 
363
384
  old_names = to_rename.invert
364
385
  changes = []
365
- undo_changes = []
366
386
  to_change.each do |col_name_to_change|
367
387
  orig_col_name = old_names[col_name_to_change] || col_name_to_change
368
388
  column = db_columns[orig_col_name] or raise "failed to find column info for #{orig_col_name.inspect}"
@@ -374,36 +394,33 @@ module Generators
374
394
 
375
395
  if !::DeclareSchema::Model::Column.equivalent_schema_attributes?(normalized_schema_attrs, col_attrs)
376
396
  type = normalized_schema_attrs.delete(:type) or raise "no :type found in #{normalized_schema_attrs.inspect}"
377
- changes << ["change_column #{new_table_name.to_sym.inspect}", col_name_to_change.to_sym.inspect,
378
- type.to_sym.inspect, *format_options(normalized_schema_attrs)].join(", ")
379
- undo_changes << change_column_back(model, current_table_name, orig_col_name)
397
+ old_type, old_options = change_column_back(model, current_table_name, orig_col_name)
398
+ changes << ::DeclareSchema::SchemaChange::ColumnChange.new(new_table_name, col_name_to_change,
399
+ new_type: type, new_options: normalized_schema_attrs,
400
+ old_type: old_type, old_options: old_options)
380
401
  end
381
402
  end
382
403
 
383
- index_changes, undo_index_changes = change_indexes(model, current_table_name, to_remove)
384
- fk_changes, undo_fk_changes = if ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
385
- [[], []]
386
- else
387
- change_foreign_key_constraints(model, current_table_name)
388
- end
389
- table_options_changes, undo_table_options_changes = if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
390
- change_table_options(model, current_table_name)
391
- else
392
- [[], []]
393
- end
394
-
395
- [(renames + adds + removes + changes) * "\n",
396
- (undo_renames + undo_adds + undo_removes + undo_changes) * "\n",
397
- index_changes * "\n",
398
- undo_index_changes * "\n",
399
- fk_changes * "\n",
400
- undo_fk_changes * "\n",
401
- table_options_changes * "\n",
402
- undo_table_options_changes * "\n"]
404
+ index_changes = change_indexes(model, current_table_name, to_rename)
405
+ fk_changes = if ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
406
+ []
407
+ else
408
+ change_foreign_key_constraints(model, current_table_name)
409
+ end
410
+ table_options_changes = if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
411
+ change_table_options(model, current_table_name)
412
+ else
413
+ []
414
+ end
415
+
416
+ [(renames + adds + removes + changes),
417
+ index_changes,
418
+ fk_changes,
419
+ table_options_changes]
403
420
  end
404
421
 
405
- def change_indexes(model, old_table_name, to_remove)
406
- ::DeclareSchema.default_generate_indexing or return [[], []]
422
+ def change_indexes(model, old_table_name, to_rename)
423
+ ::DeclareSchema.default_generate_indexing or return []
407
424
 
408
425
  new_table_name = model.table_name
409
426
  existing_indexes = ::DeclareSchema::Model::IndexDefinition.for_model(model, old_table_name)
@@ -415,69 +432,54 @@ module Generators
415
432
  end
416
433
  end || i
417
434
  end
418
- existing_has_primary_key = existing_indexes.any? do |i|
419
- i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME &&
420
- !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
435
+ existing_primary_keys, existing_indexes_without_primary_key = existing_indexes.partition { |i| i.primary_key? }
436
+ defined_primary_keys, model_indexes_without_primary_key = model_indexes.partition { |i| i.primary_key? }
437
+ existing_primary_keys.size <= 1 or raise "too many existing primary keys! #{existing_primary_keys.inspect}"
438
+ defined_primary_keys.size <= 1 or raise "too many defined primary keys! #{defined_primary_keys.inspect}"
439
+ existing_primary_key = existing_primary_keys.first
440
+ defined_primary_key = defined_primary_keys.first
441
+
442
+ existing_primary_key_columns = (existing_primary_key&.columns || []).map { |col_name| to_rename[col_name] || col_name }
443
+
444
+ if !ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
445
+ change_primary_key =
446
+ if (existing_primary_key || defined_primary_key) &&
447
+ existing_primary_key_columns != defined_primary_key&.columns
448
+ ::DeclareSchema::SchemaChange::PrimaryKeyChange.new(new_table_name, existing_primary_key_columns, defined_primary_key&.columns)
449
+ end
421
450
  end
422
- model_has_primary_key = model_indexes.any? { |i| i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME }
423
451
 
424
- undo_add_indexes = []
425
- add_indexes = (model_indexes - existing_indexes).map do |i|
426
- undo_add_indexes << drop_index(old_table_name, i.name) unless i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME
427
- i.to_add_statement(new_table_name, existing_has_primary_key)
452
+ drop_indexes = (existing_indexes_without_primary_key - model_indexes_without_primary_key).map do |i|
453
+ ::DeclareSchema::SchemaChange::IndexRemove.new(new_table_name, i.columns, unique: i.unique, where: i.where, name: i.name)
428
454
  end
429
- undo_drop_indexes = []
430
- drop_indexes = (existing_indexes - model_indexes).map do |i|
431
- undo_drop_indexes << i.to_add_statement(old_table_name, model_has_primary_key)
432
- drop_index(new_table_name, i.name) unless i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME
433
- end.compact
434
455
 
435
- # the order is important here - adding a :unique, for instance needs to remove then add
436
- [drop_indexes + add_indexes, undo_add_indexes + undo_drop_indexes]
437
- end
456
+ add_indexes = (model_indexes_without_primary_key - existing_indexes_without_primary_key).map do |i|
457
+ ::DeclareSchema::SchemaChange::IndexAdd.new(new_table_name, i.columns, unique: i.unique, where: i.where, name: i.name)
458
+ end
438
459
 
439
- def drop_index(table, name)
440
- # see https://hobo.lighthouseapp.com/projects/8324/tickets/566
441
- # for why the rescue exists
442
- "remove_index :#{table}, name: :#{name} rescue ActiveRecord::StatementInvalid"
460
+ # the order is important here - adding a :unique, for instance needs to remove then add
461
+ [Array(change_primary_key) + drop_indexes + add_indexes]
443
462
  end
444
463
 
445
464
  def change_foreign_key_constraints(model, old_table_name)
446
465
  ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/) and raise ArgumentError, 'SQLite does not support foreign keys'
447
- ::DeclareSchema.default_generate_foreign_keys or return [[], []]
466
+ ::DeclareSchema.default_generate_foreign_keys or return []
448
467
 
449
- new_table_name = model.table_name
450
468
  existing_fks = ::DeclareSchema::Model::ForeignKeyDefinition.for_model(model, old_table_name)
451
469
  model_fks = model.constraint_specs
452
470
 
453
- undo_add_fks = []
454
- add_fks = (model_fks - existing_fks).map do |fk|
455
- # next if fk.parent.constantize.abstract_class || fk.parent == fk.model.class_name
456
- undo_add_fks << remove_foreign_key(old_table_name, fk.constraint_name)
457
- fk.to_add_statement
458
- end
459
-
460
- undo_drop_fks = []
461
471
  drop_fks = (existing_fks - model_fks).map do |fk|
462
- undo_drop_fks << fk.to_add_statement
463
- remove_foreign_key(new_table_name, fk.constraint_name)
472
+ ::DeclareSchema::SchemaChange::ForeignKeyRemove.new(fk.child_table_name, fk.parent_table_name,
473
+ column_name: fk.foreign_key_name, name: fk.constraint_name)
464
474
  end
465
475
 
466
- [drop_fks + add_fks, undo_add_fks + undo_drop_fks]
467
- end
468
-
469
- def remove_foreign_key(old_table_name, fk_name)
470
- "remove_foreign_key(#{old_table_name.inspect}, name: #{fk_name.to_s.inspect})"
471
- end
472
-
473
- def format_options(options)
474
- options.map do |k, v|
475
- if k.is_a?(Symbol)
476
- "#{k}: #{v.inspect}"
477
- else
478
- "#{k.inspect} => #{v.inspect}"
479
- end
476
+ add_fks = (model_fks - existing_fks).map do |fk|
477
+ # next if fk.parent.constantize.abstract_class || fk.parent == fk.model.class_name
478
+ ::DeclareSchema::SchemaChange::ForeignKeyAdd.new(fk.child_table_name, fk.parent_table_name,
479
+ column_name: fk.foreign_key_name, name: fk.constraint_name)
480
480
  end
481
+
482
+ [drop_fks + add_fks]
481
483
  end
482
484
 
483
485
  def fk_field_options(model, field_name)
@@ -486,7 +488,7 @@ module Generators
486
488
  parent_columns = connection.columns(parent_table) rescue []
487
489
  pk_limit =
488
490
  if (pk_column = parent_columns.find { |column| column.name.to_s == "id" }) # right now foreign keys assume id is the target
489
- if Rails::VERSION::MAJOR < 5
491
+ if ActiveSupport::VERSION::MAJOR < 5
490
492
  pk_column.cast_type.limit
491
493
  else
492
494
  pk_column.limit
@@ -506,11 +508,12 @@ module Generators
506
508
  new_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, table_options_for_model(model))
507
509
 
508
510
  if old_options_definition.equivalent?(new_options_definition)
509
- [[], []]
511
+ []
510
512
  else
511
513
  [
512
- [new_options_definition.alter_table_statement],
513
- [old_options_definition.alter_table_statement]
514
+ ::DeclareSchema::SchemaChange::TableChange.new(current_table_name,
515
+ old_options_definition.settings,
516
+ new_options_definition.settings)
514
517
  ]
515
518
  end
516
519
  end
@@ -528,7 +531,7 @@ module Generators
528
531
  col_spec = ::DeclareSchema::Model::Column.new(model, current_table_name, column)
529
532
  schema_attributes = col_spec.schema_attributes
530
533
  type = schema_attributes.delete(:type) or raise "no :type found in #{schema_attributes.inspect}"
531
- ["add_column :#{current_table_name}, :#{col_name}, #{type.inspect}", *format_options(schema_attributes)].join(', ')
534
+ [type, schema_attributes]
532
535
  end
533
536
  end
534
537
 
@@ -538,7 +541,7 @@ module Generators
538
541
  col_spec = ::DeclareSchema::Model::Column.new(model, current_table_name, column)
539
542
  schema_attributes = col_spec.schema_attributes
540
543
  type = schema_attributes.delete(:type) or raise "no :type found in #{schema_attributes.inspect}"
541
- ["change_column #{current_table_name.to_sym.inspect}", col_name.to_sym.inspect, type.to_sym.inspect, *format_options(schema_attributes)].join(', ')
544
+ [type, schema_attributes]
542
545
  end
543
546
  end
544
547
 
@@ -551,7 +554,7 @@ module Generators
551
554
  end
552
555
  end
553
556
 
554
- SchemaDumper = case Rails::VERSION::MAJOR
557
+ SchemaDumper = case ActiveSupport::VERSION::MAJOR
555
558
  when 4
556
559
  ActiveRecord::SchemaDumper
557
560
  else