Class: Generators::DeclareSchema::Migration::Migrator

Inherits:
Object
  • Object
show all
Defined in:
lib/generators/declare_schema/migration/migrator.rb

Defined Under Namespace

Classes: Error

Constant Summary collapse

MIGRATION_ORDER =
%w[ TableRename
TableAdd
TableChange
  ColumnAdd
  ColumnRename
  ColumnChange
    PrimaryKeyChange
    ForeignKeyRemove
      IndexRemove
      IndexAdd
    ForeignKeyAdd
  ColumnRemove
TableRemove ]

Class Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(renames: nil, &block) ⇒ Migrator

Returns a new instance of Migrator.



50
51
52
53
54
# File 'lib/generators/declare_schema/migration/migrator.rb', line 50

def initialize(renames: nil, &block)
  @ambiguity_resolver = block
  @drops = []
  @renames = renames
end

Class Attribute Details

.active_record_classObject (readonly)

Returns the value of attribute active_record_class.



21
22
23
# File 'lib/generators/declare_schema/migration/migrator.rb', line 21

def active_record_class
  @active_record_class
end

.before_generating_migration_callbackObject (readonly)

Returns the value of attribute before_generating_migration_callback.



21
22
23
# File 'lib/generators/declare_schema/migration/migrator.rb', line 21

def before_generating_migration_callback
  @before_generating_migration_callback
end

.ignore_modelsObject

Returns the value of attribute ignore_models.



20
21
22
# File 'lib/generators/declare_schema/migration/migrator.rb', line 20

def ignore_models
  @ignore_models
end

.ignore_tablesObject

Returns the value of attribute ignore_tables.



20
21
22
# File 'lib/generators/declare_schema/migration/migrator.rb', line 20

def ignore_tables
  @ignore_tables
end

Class Method Details

.always_ignore_tablesObject



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/generators/declare_schema/migration/migrator.rb', line 168

def self.always_ignore_tables
  sessions_table =
    begin
      if defined?(CGI::Session::ActiveRecordStore::Session) &&
         defined?(ActionController::Base) &&
         ActionController::Base.session_store == CGI::Session::ActiveRecordStore
        CGI::Session::ActiveRecordStore::Session.table_name
      end
    rescue
      nil
    end

  [
    'schema_info',
    ActiveRecord::Base.try(:schema_migrations_table_name) || 'schema_migrations',
    ActiveRecord::Base.try(:internal_metadata_table_name) || 'ar_internal_metadata',
    sessions_table
  ].compact
end

.before_generating_migration(&block) ⇒ Object



41
42
43
44
# File 'lib/generators/declare_schema/migration/migrator.rb', line 41

def before_generating_migration(&block)
  block or raise ArgumentError, 'A block is required when setting the before_generating_migration callback'
  @before_generating_migration_callback = block
end

.connectionObject



37
38
39
# File 'lib/generators/declare_schema/migration/migrator.rb', line 37

def connection
  ActiveRecord::Base.connection
end

.default_migration_name(existing_migrations = ) ⇒ Object



32
33
34
35
# File 'lib/generators/declare_schema/migration/migrator.rb', line 32

def default_migration_name(existing_migrations = Dir["#{Rails.root}/db/migrate/*declare_schema_migration*"])
  max = existing_migrations.grep(/([0-9]+)\.rb$/) { Regexp.last_match(1).to_i }.max.to_i
  "declare_schema_migration_#{max + 1}"
end

.run(**renames) ⇒ Object



28
29
30
# File 'lib/generators/declare_schema/migration/migrator.rb', line 28

def run(**renames)
  Migrator.new(renames: renames).generate
end

Instance Method Details

#connectionObject



75
76
77
# File 'lib/generators/declare_schema/migration/migrator.rb', line 75

def connection
  self.class.connection
end

#extract_column_renames!(to_add, to_remove, table_name) ⇒ Object

return a hash of column renames and modifies the passed arrays so that renamed columns are no longer listed as to_create or to_drop



142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
# File 'lib/generators/declare_schema/migration/migrator.rb', line 142

def extract_column_renames!(to_add, to_remove, table_name)
  if @renames
    to_rename = {}
    if (column_renames = @renames[table_name.to_sym])
      # A hash of column renames has been provided

      column_renames.each do |old_name, new_name|
        old_name = old_name.to_s
        new_name = new_name.to_s
        to_add.delete(new_name) or raise Error,
          "Rename specified new name: #{new_name.inspect} but it was not in the `to_add` list for table #{table_name}"
        to_remove.delete(old_name) or raise Error,
          "Rename specified old name: #{old_name.inspect} but it was not in the `to_remove` list for table #{table_name}"
        to_rename[old_name] = new_name
      end
    end
    to_rename

  elsif @ambiguity_resolver
    @ambiguity_resolver.call(to_add, to_remove, "column", "#{table_name}.")

  else
    raise Error, "Unable to resolve migration ambiguities in table #{table_name}"
  end
end

#extract_table_renames!(to_create, to_drop) ⇒ Object

return a hash of table renames and modifies the passed arrays so that renamed tables are no longer listed as to_create or to_drop



110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/generators/declare_schema/migration/migrator.rb', line 110

def extract_table_renames!(to_create, to_drop)
  if @renames
    # A hash of table renames has been provided

    to_rename = {}
    @renames.each do |old_name, new_name|
      if new_name.is_a?(Hash)
        new_name = new_name[:table_name]
      end
      new_name or next

      old_name = old_name.to_s
      new_name = new_name.to_s

      to_create.delete(new_name) or raise Error,
        "Rename specified new name: #{new_name.inspect} but it was not in the `to_create` list"
      to_drop.delete(old_name) or raise Error,
        "Rename specified old name: #{old_name.inspect} but it was not in the `to_drop` list"
      to_rename[old_name] = new_name
    end
    to_rename

  elsif @ambiguity_resolver
    @ambiguity_resolver.call(to_create, to_drop, "table", nil)

  else
    raise Error, "Unable to resolve migration ambiguities"
  end
end

#generateObject



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/generators/declare_schema/migration/migrator.rb', line 188

def generate
  models, db_tables = models_and_tables
  models_by_table_name = {}
  models.each do |m|
    m.try(:field_specs)&.each do |_name, field_spec|
      if (pre_migration = field_spec.options.delete(:pre_migration))
        pre_migration.call(field_spec)
      end
    end

    if !models_by_table_name.has_key?(m.table_name)
      models_by_table_name[m.table_name] = m
    elsif m.superclass == models_by_table_name[m.table_name].superclass.superclass
      # we need to ensure that models_by_table_name contains the
      # base class in an STI hierarchy
      models_by_table_name[m.table_name] = m
    end
  end
  # generate shims for HABTM models
  habtm_tables.each do |name, reflections|
    models_by_table_name[name] = ::DeclareSchema::Model::HabtmModelShim.from_reflection(reflections.first)
  end
  model_table_names = models_by_table_name.keys

  to_create = model_table_names - db_tables
  to_drop = db_tables - model_table_names - self.class.always_ignore_tables
  to_rename = extract_table_renames!(to_create, to_drop)
  to_change = model_table_names

  renames = to_rename.map do |old_name, new_name|
    ::DeclareSchema::SchemaChange::TableRename.new(old_name, new_name)
  end

  drops = to_drop.map do |t|
    ::DeclareSchema::SchemaChange::TableRemove.new(t, add_table_back(t))
  end

  creates = to_create.map do |table_name|
    model = models_by_table_name[table_name]
    disable_auto_increment = model.try(:disable_auto_increment)

    primary_key_definition =
      if disable_auto_increment
        [[:integer, :id, limit: 8, auto_increment: false, primary_key: true]]
      else
        []
      end

    field_definitions = model.field_specs.values.sort_by(&:position).map do |f|
      [f.type, f.name, f.sql_options]
    end

    table_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, **table_options_for_model(model))
    table_options = create_table_options(model, disable_auto_increment)

    table_add = ::DeclareSchema::SchemaChange::TableAdd.new(
      table_name,
      primary_key_definition + field_definitions,
      table_options,
      sql_options: table_options_definition.settings
    )

    [
      table_add,
      *Array((create_indexes(model)     if ::DeclareSchema.default_generate_indexing)),
      *Array((create_constraints(model) if ::DeclareSchema.default_generate_foreign_keys))
    ]
  end

  changes                    = []
  index_changes              = []
  fk_changes                 = []
  table_options_changes      = []

  to_change.each do |table_name|
    model = models_by_table_name[table_name]
    table = to_rename.key(table_name) || model.table_name
    if table.in?(db_tables)
      change, index_change, fk_change, table_options_change = change_table(model, table)
      changes << change
      index_changes << index_change
      fk_changes << fk_change
      table_options_changes << table_options_change
    end
  end

  migration_commands = [renames, drops, creates, changes, index_changes, fk_changes, table_options_changes].flatten

  ordered_migration_commands = order_migrations(migration_commands)

  up_and_down_migrations(ordered_migration_commands)
end

#habtm_tablesObject

list habtm join tables



84
85
86
87
88
89
90
91
92
# File 'lib/generators/declare_schema/migration/migrator.rb', line 84

def habtm_tables
  reflections = Hash.new { |h, k| h[k] = [] }
  ActiveRecord::Base.send(:descendants).map do |c|
    c.reflect_on_all_associations(:has_and_belongs_to_many).each do |a|
      reflections[a.join_table] << a
    end
  end
  reflections
end

#load_rails_modelsObject



56
57
58
59
60
61
62
63
64
# File 'lib/generators/declare_schema/migration/migrator.rb', line 56

def load_rails_models
  ActiveRecord::Migration.verbose = false
  if defined?(Rails)
    Rails.application or raise "Rails is defined, so Rails.application must be set"
    Rails.application.eager_load!
    Rails::Engine.subclasses.each(&:eager_load!)
  end
  self.class.before_generating_migration_callback&.call
end

#models_and_tablesObject

Returns an array of model classes and an array of table names that generation needs to take into account



96
97
98
99
100
101
102
103
104
105
106
# File 'lib/generators/declare_schema/migration/migrator.rb', line 96

def models_and_tables
  ignore_model_names = Migrator.ignore_models.map { |model| model.to_s.underscore }
  all_models = table_model_classes
  declare_schema_models = all_models.select do |m|
    (m.name['HABTM_'] ||
      (m.include_in_migration if m.respond_to?(:include_in_migration))) && !m.name.underscore.in?(ignore_model_names)
  end
  non_declare_schema_models = all_models - declare_schema_models
  db_tables = connection.tables - Migrator.ignore_tables.map(&:to_s) - non_declare_schema_models.map(&:table_name)
  [declare_schema_models, db_tables]
end

#native_typesObject



79
80
81
# File 'lib/generators/declare_schema/migration/migrator.rb', line 79

def native_types
  self.class.native_types
end

#order_migrations(migration_commands) ⇒ Object



295
296
297
298
299
300
301
# File 'lib/generators/declare_schema/migration/migrator.rb', line 295

def order_migrations(migration_commands)
  migration_commands.each_with_index.sort_by do |command, index|
    command_type = command.class.name.gsub(/.*::/, '')
    priority = MIGRATION_ORDER.index(command_type) or raise "#{command_type.inspect} not found in #{MIGRATION_ORDER.inspect}"
    [priority, index] # index keeps the sort stable in case of a tie
  end.map(&:first) # remove the index
end

#table_model_classesObject

Returns an array of model classes that directly extend ActiveRecord::Base, excluding anything in the CGI module



68
69
70
71
72
73
# File 'lib/generators/declare_schema/migration/migrator.rb', line 68

def table_model_classes
  load_rails_models
  ActiveRecord::Base.send(:descendants).select do |klass|
    klass.base_class == klass && !klass.name.starts_with?("CGI::")
  end
end