declare_schema 0.10.0.pre.dc.1 → 0.12.0.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -2
  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/dsl.rb +1 -2
  7. data/lib/declare_schema/extensions/active_record/fields_declaration.rb +4 -2
  8. data/lib/declare_schema/model.rb +59 -15
  9. data/lib/declare_schema/model/column.rb +2 -2
  10. data/lib/declare_schema/model/field_spec.rb +4 -4
  11. data/lib/declare_schema/model/foreign_key_definition.rb +6 -11
  12. data/lib/declare_schema/model/habtm_model_shim.rb +2 -2
  13. data/lib/declare_schema/model/index_definition.rb +8 -25
  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/USAGE +14 -24
  31. data/lib/generators/declare_schema/migration/migration_generator.rb +40 -38
  32. data/lib/generators/declare_schema/migration/migrator.rb +190 -187
  33. data/lib/generators/declare_schema/migration/templates/migration.rb.erb +3 -3
  34. data/spec/lib/declare_schema/api_spec.rb +3 -1
  35. data/spec/lib/declare_schema/field_spec_spec.rb +3 -3
  36. data/spec/lib/declare_schema/generator_spec.rb +2 -2
  37. data/spec/lib/declare_schema/interactive_primary_key_spec.rb +60 -25
  38. data/spec/lib/declare_schema/migration_generator_spec.rb +471 -377
  39. data/spec/lib/declare_schema/model/column_spec.rb +2 -6
  40. data/spec/lib/declare_schema/model/foreign_key_definition_spec.rb +28 -16
  41. data/spec/lib/declare_schema/model/habtm_model_shim_spec.rb +4 -6
  42. data/spec/lib/declare_schema/model/index_definition_spec.rb +4 -4
  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/generators/declare_schema/migration/migrator_spec.rb +59 -11
  58. data/spec/spec_helper.rb +1 -1
  59. data/spec/support/acceptance_spec_helpers.rb +2 -2
  60. metadata +34 -5
@@ -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.10.0.pre.dc.1"
4
+ VERSION = "0.12.0.pre.1"
5
5
  end
@@ -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] unless 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