declare_schema 0.8.0 → 0.11.0

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/declare_schema_build.yml +1 -1
  3. data/CHANGELOG.md +37 -0
  4. data/Gemfile.lock +1 -1
  5. data/README.md +102 -14
  6. data/lib/declare_schema.rb +57 -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 +4 -6
  10. data/lib/declare_schema/model/column.rb +1 -1
  11. data/lib/declare_schema/model/field_spec.rb +9 -6
  12. data/lib/declare_schema/model/foreign_key_definition.rb +6 -11
  13. data/lib/declare_schema/model/index_definition.rb +1 -20
  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 +184 -198
  33. data/lib/generators/declare_schema/migration/templates/migration.rb.erb +1 -1
  34. data/lib/generators/declare_schema/support/model.rb +4 -4
  35. data/spec/lib/declare_schema/api_spec.rb +8 -8
  36. data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +41 -15
  37. data/spec/lib/declare_schema/field_spec_spec.rb +44 -2
  38. data/spec/lib/declare_schema/generator_spec.rb +5 -5
  39. data/spec/lib/declare_schema/interactive_primary_key_spec.rb +117 -28
  40. data/spec/lib/declare_schema/migration_generator_spec.rb +1990 -843
  41. data/spec/lib/declare_schema/model/column_spec.rb +49 -23
  42. data/spec/lib/declare_schema/model/foreign_key_definition_spec.rb +158 -57
  43. data/spec/lib/declare_schema/model/habtm_model_shim_spec.rb +0 -2
  44. data/spec/lib/declare_schema/model/index_definition_spec.rb +189 -78
  45. data/spec/lib/declare_schema/model/table_options_definition_spec.rb +75 -11
  46. data/spec/lib/declare_schema/schema_change/base_spec.rb +75 -0
  47. data/spec/lib/declare_schema/schema_change/column_add_spec.rb +30 -0
  48. data/spec/lib/declare_schema/schema_change/column_change_spec.rb +33 -0
  49. data/spec/lib/declare_schema/schema_change/column_remove_spec.rb +30 -0
  50. data/spec/lib/declare_schema/schema_change/column_rename_spec.rb +28 -0
  51. data/spec/lib/declare_schema/schema_change/foreign_key_add_spec.rb +29 -0
  52. data/spec/lib/declare_schema/schema_change/foreign_key_remove_spec.rb +29 -0
  53. data/spec/lib/declare_schema/schema_change/index_add_spec.rb +56 -0
  54. data/spec/lib/declare_schema/schema_change/index_remove_spec.rb +29 -0
  55. data/spec/lib/declare_schema/schema_change/primary_key_change_spec.rb +69 -0
  56. data/spec/lib/declare_schema/schema_change/table_add_spec.rb +50 -0
  57. data/spec/lib/declare_schema/schema_change/table_change_spec.rb +30 -0
  58. data/spec/lib/declare_schema/schema_change/table_remove_spec.rb +27 -0
  59. data/spec/lib/declare_schema/schema_change/table_rename_spec.rb +27 -0
  60. data/spec/lib/declare_schema_spec.rb +101 -0
  61. data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +71 -13
  62. data/spec/spec_helper.rb +1 -1
  63. data/spec/support/acceptance_spec_helpers.rb +2 -2
  64. metadata +36 -6
  65. data/test_responses.txt +0 -2
@@ -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"
4
+ VERSION = "0.11.0"
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, 4)
77
+ @down = indent(down, 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
@@ -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 = {})
64
- @ambiguity_resolver = ambiguity_resolver
50
+ def initialize(renames: nil, &block)
51
+ @ambiguity_resolver = block
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,85 +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
300
  primary_key = model._defined_primary_key
289
301
  if primary_key.blank? || disable_auto_increment
290
- "id: false"
302
+ { id: false }
291
303
  elsif primary_key == "id"
292
- "id: :bigint"
304
+ { id: :bigint }
293
305
  else
294
- "primary_key: :#{primary_key}"
306
+ { primary_key: primary_key.to_sym }
295
307
  end
296
308
  end
297
309
 
@@ -300,24 +312,24 @@ module Generators
300
312
  {}
301
313
  else
302
314
  {
303
- charset: model.table_options[:charset] || Migrator.default_charset,
304
- 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
305
317
  }
306
318
  end
307
319
  end
308
320
 
321
+ # TODO: TECH-5338: optimize that index doesn't need to be dropped on undo since entire table will be dropped
309
322
  def create_indexes(model)
310
- 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
311
326
  end
312
327
 
313
328
  def create_constraints(model)
314
- model.constraint_specs.map { |fk| fk.to_add_statement }
315
- end
316
-
317
- def create_field(field_spec, field_name_width)
318
- options = field_spec.sql_options.merge(fk_field_options(field_spec.model, field_spec.name))
319
- args = [field_spec.name.inspect] + format_options(options.compact)
320
- 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
321
333
  end
322
334
 
323
335
  def change_table(model, current_table_name)
@@ -344,37 +356,28 @@ module Generators
344
356
  to_change = db_column_names & model_column_names
345
357
 
346
358
  renames = to_rename.map do |old_name, new_name|
347
- "rename_column :#{new_table_name}, :#{old_name}, :#{new_name}"
348
- end
349
- undo_renames = to_rename.map do |old_name, new_name|
350
- "rename_column :#{new_table_name}, :#{new_name}, :#{old_name}"
359
+ ::DeclareSchema::SchemaChange::ColumnRename.new(new_table_name, old_name, new_name)
351
360
  end
352
361
 
353
- 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
+
354
364
  adds = to_add.map do |c|
355
- args =
365
+ type, options =
356
366
  if (spec = model.field_specs[c])
357
- options = spec.sql_options.merge(fk_field_options(model, c))
358
- [":#{spec.type}", *format_options(options.compact)]
367
+ [spec.type, spec.sql_options.merge(fk_field_options(model, c)).compact]
359
368
  else
360
- [":integer"]
369
+ [:integer, {}]
361
370
  end
362
- ["add_column :#{new_table_name}, :#{c}", *args].join(', ')
363
- end
364
- undo_adds = to_add.map do |c|
365
- "remove_column :#{new_table_name}, :#{c}"
371
+ ::DeclareSchema::SchemaChange::ColumnAdd.new(new_table_name, c, type, options)
366
372
  end
367
373
 
368
374
  removes = to_remove.map do |c|
369
- "remove_column :#{new_table_name}, :#{c}"
370
- end
371
- undo_removes = to_remove.map do |c|
372
- 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)
373
377
  end
374
378
 
375
379
  old_names = to_rename.invert
376
380
  changes = []
377
- undo_changes = []
378
381
  to_change.each do |col_name_to_change|
379
382
  orig_col_name = old_names[col_name_to_change] || col_name_to_change
380
383
  column = db_columns[orig_col_name] or raise "failed to find column info for #{orig_col_name.inspect}"
@@ -386,36 +389,33 @@ module Generators
386
389
 
387
390
  if !::DeclareSchema::Model::Column.equivalent_schema_attributes?(normalized_schema_attrs, col_attrs)
388
391
  type = normalized_schema_attrs.delete(:type) or raise "no :type found in #{normalized_schema_attrs.inspect}"
389
- changes << ["change_column #{new_table_name.to_sym.inspect}", col_name_to_change.to_sym.inspect,
390
- type.to_sym.inspect, *format_options(normalized_schema_attrs)].join(", ")
391
- 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)
392
396
  end
393
397
  end
394
398
 
395
- index_changes, undo_index_changes = change_indexes(model, current_table_name, to_remove)
396
- fk_changes, undo_fk_changes = if ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
397
- [[], []]
398
- else
399
- change_foreign_key_constraints(model, current_table_name)
400
- end
401
- table_options_changes, undo_table_options_changes = if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
402
- change_table_options(model, current_table_name)
403
- else
404
- [[], []]
405
- end
406
-
407
- [(renames + adds + removes + changes) * "\n",
408
- (undo_renames + undo_adds + undo_removes + undo_changes) * "\n",
409
- index_changes * "\n",
410
- undo_index_changes * "\n",
411
- fk_changes * "\n",
412
- undo_fk_changes * "\n",
413
- table_options_changes * "\n",
414
- 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]
415
415
  end
416
416
 
417
- def change_indexes(model, old_table_name, to_remove)
418
- Migrator.disable_constraints and return [[], []]
417
+ def change_indexes(model, old_table_name, to_rename)
418
+ ::DeclareSchema.default_generate_indexing or return []
419
419
 
420
420
  new_table_name = model.table_name
421
421
  existing_indexes = ::DeclareSchema::Model::IndexDefinition.for_model(model, old_table_name)
@@ -427,69 +427,54 @@ module Generators
427
427
  end
428
428
  end || i
429
429
  end
430
- existing_has_primary_key = existing_indexes.any? do |i|
431
- i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME &&
432
- !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
433
445
  end
434
- model_has_primary_key = model_indexes.any? { |i| i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME }
435
446
 
436
- undo_add_indexes = []
437
- add_indexes = (model_indexes - existing_indexes).map do |i|
438
- undo_add_indexes << drop_index(old_table_name, i.name) unless i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME
439
- 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)
440
449
  end
441
- undo_drop_indexes = []
442
- drop_indexes = (existing_indexes - model_indexes).map do |i|
443
- undo_drop_indexes << i.to_add_statement(old_table_name, model_has_primary_key)
444
- drop_index(new_table_name, i.name) unless i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME
445
- end.compact
446
450
 
447
- # the order is important here - adding a :unique, for instance needs to remove then add
448
- [drop_indexes + add_indexes, undo_add_indexes + undo_drop_indexes]
449
- 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
450
454
 
451
- def drop_index(table, name)
452
- # see https://hobo.lighthouseapp.com/projects/8324/tickets/566
453
- # for why the rescue exists
454
- "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]
455
457
  end
456
458
 
457
459
  def change_foreign_key_constraints(model, old_table_name)
458
460
  ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/) and raise ArgumentError, 'SQLite does not support foreign keys'
459
- Migrator.disable_indexing and return [[], []]
461
+ ::DeclareSchema.default_generate_foreign_keys or return []
460
462
 
461
- new_table_name = model.table_name
462
463
  existing_fks = ::DeclareSchema::Model::ForeignKeyDefinition.for_model(model, old_table_name)
463
464
  model_fks = model.constraint_specs
464
465
 
465
- undo_add_fks = []
466
- add_fks = (model_fks - existing_fks).map do |fk|
467
- # next if fk.parent.constantize.abstract_class || fk.parent == fk.model.class_name
468
- undo_add_fks << remove_foreign_key(old_table_name, fk.constraint_name)
469
- fk.to_add_statement
470
- end
471
-
472
- undo_drop_fks = []
473
466
  drop_fks = (existing_fks - model_fks).map do |fk|
474
- undo_drop_fks << fk.to_add_statement
475
- 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)
476
469
  end
477
470
 
478
- [drop_fks + add_fks, undo_add_fks + undo_drop_fks]
479
- end
480
-
481
- def remove_foreign_key(old_table_name, fk_name)
482
- "remove_foreign_key(#{old_table_name.inspect}, name: #{fk_name.to_s.inspect})"
483
- end
484
-
485
- def format_options(options)
486
- options.map do |k, v|
487
- if k.is_a?(Symbol)
488
- "#{k}: #{v.inspect}"
489
- else
490
- "#{k.inspect} => #{v.inspect}"
491
- 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)
492
475
  end
476
+
477
+ [drop_fks + add_fks]
493
478
  end
494
479
 
495
480
  def fk_field_options(model, field_name)
@@ -498,7 +483,7 @@ module Generators
498
483
  parent_columns = connection.columns(parent_table) rescue []
499
484
  pk_limit =
500
485
  if (pk_column = parent_columns.find { |column| column.name.to_s == "id" }) # right now foreign keys assume id is the target
501
- if Rails::VERSION::MAJOR < 5
486
+ if ActiveSupport::VERSION::MAJOR < 5
502
487
  pk_column.cast_type.limit
503
488
  else
504
489
  pk_column.limit
@@ -518,11 +503,12 @@ module Generators
518
503
  new_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, table_options_for_model(model))
519
504
 
520
505
  if old_options_definition.equivalent?(new_options_definition)
521
- [[], []]
506
+ []
522
507
  else
523
508
  [
524
- [new_options_definition.alter_table_statement],
525
- [old_options_definition.alter_table_statement]
509
+ ::DeclareSchema::SchemaChange::TableChange.new(current_table_name,
510
+ old_options_definition.settings,
511
+ new_options_definition.settings)
526
512
  ]
527
513
  end
528
514
  end
@@ -540,7 +526,7 @@ module Generators
540
526
  col_spec = ::DeclareSchema::Model::Column.new(model, current_table_name, column)
541
527
  schema_attributes = col_spec.schema_attributes
542
528
  type = schema_attributes.delete(:type) or raise "no :type found in #{schema_attributes.inspect}"
543
- ["add_column :#{current_table_name}, :#{col_name}, #{type.inspect}", *format_options(schema_attributes)].join(', ')
529
+ [type, schema_attributes]
544
530
  end
545
531
  end
546
532
 
@@ -550,7 +536,7 @@ module Generators
550
536
  col_spec = ::DeclareSchema::Model::Column.new(model, current_table_name, column)
551
537
  schema_attributes = col_spec.schema_attributes
552
538
  type = schema_attributes.delete(:type) or raise "no :type found in #{schema_attributes.inspect}"
553
- ["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]
554
540
  end
555
541
  end
556
542
 
@@ -563,7 +549,7 @@ module Generators
563
549
  end
564
550
  end
565
551
 
566
- SchemaDumper = case Rails::VERSION::MAJOR
552
+ SchemaDumper = case ActiveSupport::VERSION::MAJOR
567
553
  when 4
568
554
  ActiveRecord::SchemaDumper
569
555
  else
@@ -585,8 +571,8 @@ module Generators
585
571
  # TODO: rewrite this method to use charset and collation variables rather than manipulating strings. -Colin
586
572
  def fix_mysql_charset_and_collation(dumped_schema)
587
573
  if !dumped_schema['options: ']
588
- dumped_schema.sub!('",', "\", options: \"DEFAULT CHARSET=#{Generators::DeclareSchema::Migration::Migrator.default_charset} "+
589
- "COLLATE=#{Generators::DeclareSchema::Migration::Migrator.default_collation}\",")
574
+ dumped_schema.sub!('",', "\", options: \"DEFAULT CHARSET=#{::DeclareSchema.default_charset} "+
575
+ "COLLATE=#{::DeclareSchema.default_collation}\",")
590
576
  end
591
577
  default_charset = dumped_schema[/CHARSET=(\w+)/, 1] or raise "unable to find charset in #{dumped_schema.inspect}"
592
578
  default_collation = dumped_schema[/COLLATE=(\w+)/, 1] || default_collation_from_charset(default_charset) or