declare_schema 0.8.0 → 0.11.0

Sign up to get free protection for your applications and to get access to all the features.
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