Module: MiniRecord::AutoSchema::ClassMethods

Defined in:
lib/mini_record/auto_schema.rb

Instance Method Summary collapse

Instance Method Details

#add_foreign_keys(dry_run) ⇒ Object

Add foreign keys for indexes with :foreign=>true option, if the key doesn’t exists



219
220
221
222
223
224
225
226
227
228
229
230
231
# File 'lib/mini_record/auto_schema.rb', line 219

def add_foreign_keys(dry_run)
  indexes.each do |name, options|
    if options[:foreign]
      column = options[:column].to_s
      unless foreign_keys.detect { |fk| fk[:options][:column] == column }
        to_table = reflect_on_all_associations.detect { |a| a.foreign_key.to_s==column }.table_name
        logger.debug "[MiniRecord] Adding Foreign Key on #{table_name} to #{to_table}" if logger
        connection.add_foreign_key(table_name, to_table, options) unless dry_run
        foreign_keys << { :options=> { :column=>column } }
      end
    end
  end
end

#add_index(column_name, options = {}) ⇒ Object Also known as: index



166
167
168
169
170
# File 'lib/mini_record/auto_schema.rb', line 166

def add_index(column_name, options={})
  index_name = connection.index_name(table_name, :column => column_name)
  indexes[index_name] = options.merge(:column => column_name) unless indexes.key?(index_name)
  index_name
end

#auto_upgrade!(dry_run = false) ⇒ Object



283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
# File 'lib/mini_record/auto_schema.rb', line 283

def auto_upgrade!(dry_run = false)
  return unless connection?
  return if respond_to?(:abstract_class?) && abstract_class?

  if self == ActiveRecord::Base
    descendants.each { |model| model.auto_upgrade!(dry_run) }
    clear_tables!(dry_run)
  else
    # If table doesn't exist, create it
    unless connection.tables.include?(table_name)
      class << connection; attr_accessor :table_definition; end unless connection.respond_to?(:table_definition=)
      logger.debug "[MiniRecord] Creating Table #{table_name}" if logger
      unless dry_run
        connection.table_definition = table_definition
        connection.create_table(table_name, *create_table_options)
        connection.table_definition = init_table_definition(connection)
      end
    end

    # Add this to our schema tables
    schema_tables << table_name unless schema_tables.include?(table_name)

    # Generate fields from associations
    if reflect_on_all_associations.any?
      reflect_on_all_associations.each do |association|
        foreign_key = association.options[:foreign_key] || "#{association.name}_id"
        type_key    = "#{association.name.to_s}_type"
        case association.macro
        when :belongs_to
          field foreign_key, :as => :integer unless fields.key?(foreign_key.to_s)
          if association.options[:polymorphic]
            field type_key, :as => :string unless fields.key?(type_key.to_s)
            index [foreign_key, type_key] unless suppressed_indexes[association.name]
          else
            index foreign_key unless suppressed_indexes[association.name]
          end
        when :has_and_belongs_to_many
          table = if name = association.options[:join_table]
                    name.to_s
                  else
                    association_table_name = association.name.to_s.classify.constantize.table_name
                    table_name_substrings  = [table_name,association_table_name].collect { |string| string.split('_') }
                    common_substrings      = Array.new
                    table_name_substrings.first.each_index { |i| table_name_substrings.first[i] == table_name_substrings.last[i] ? common_substrings.push(table_name_substrings.first[i]) : break }
                    common_prefix          = common_substrings.join('_')
                    table_names            = [table_name.clone,association_table_name.clone].sort
                    table_names.last.gsub!(/^#{common_prefix}_/,'')
                    table_names.join("_")
                  end
          unless connection.tables.include?(table.to_s)
            foreign_key             = association.options[:foreign_key] || association.foreign_key
            association_foreign_key = association.options[:association_foreign_key] || association.association_foreign_key
            logger.debug "[MiniRecord] Creating Join Table #{table} with keys #{foreign_key} and #{association_foreign_key}" if logger
            unless dry_run
              connection.create_table(table, :id => false) do |t|
                t.integer foreign_key
                t.integer association_foreign_key
              end
            end
            index_name = connection.index_name(table, :column => [foreign_key, association_foreign_key])
            index_name = index_name[0...connection.index_name_length] if index_name.length > connection.index_name_length
            logger.debug "[MiniRecord] Creating Join Table Index #{index_name} (#{foreign_key}, #{association_foreign_key}) on #{table}" if logger
            connection.add_index table, [foreign_key, association_foreign_key], :name => index_name, :unique => true unless dry_run or suppressed_indexes[association.name]
          end
          # Add join table to our schema tables
          schema_tables << table unless schema_tables.include?(table)
        end
      end
    end

    # Add to schema inheritance column if necessary
    if descendants.present?
      field inheritance_column, :as => :string unless fields.key?(inheritance_column.to_s)
      index inheritance_column
    end

    # Group Destructive Actions
    if MiniRecord.configuration.destructive == true and connection.tables.include?(table_name)

      # Rename fields
      rename_fields.each do |old_name, new_name|
        old_column = fields_in_db[old_name.to_s]
        new_column = fields_in_db[new_name.to_s]
        if old_column && !new_column
          logger.debug "[MiniRecord] Renaming column #{table_name}.#{old_column.name} to #{new_name}" if logger
          connection.rename_column(table_name, old_column.name, new_name) unless dry_run
        end
      end

      # Remove fields from db no longer in schema
      columns_to_delete = fields_in_db.keys - fields.keys & fields_in_db.keys
      columns_to_delete.each do |field|
        column = fields_in_db[field]
        logger.debug "[MiniRecord] Removing column #{table_name}.#{column.name}" if logger
        connection.remove_column table_name, column.name unless dry_run
      end

      # Change attributes of existent columns
      (fields.keys & fields_in_db.keys).each do |field|
        if field != primary_key #ActiveRecord::Base.get_primary_key(table_name)
          new_attr, changed = field_attr_changes(field)

          # Change the column if applicable
          new_type = fields[field].type.to_sym
          if changed
            logger.debug "[MiniRecord] Changing column #{table_name}.#{field} to new type #{new_type}" if logger
            connection.change_column table_name, field, new_type, new_attr unless dry_run
          end
        end
      end

      remove_foreign_keys(dry_run) if connection.respond_to?(:foreign_keys)

      # Remove old index
      index_names = indexes.collect{ |name, opts| (opts[:name] || name).to_s }
      (indexes_in_db.keys - index_names).each do |name|
        logger.debug "[MiniRecord] Removing index #{name} on #{table_name}" if logger
        connection.remove_index(table_name, :name => name) unless dry_run
      end

    end

    if connection.tables.include?(table_name)
      # Add fields to db new to schema
      columns_to_add = fields.keys - fields_in_db.keys
      columns_to_add.each do |field|
        column  = fields[field]
        options = {:limit => column.limit, :precision => column.precision, :scale => column.scale}
        options[:default] = column.default unless column.default.nil?
        options[:null]    = column.null    unless column.null.nil?
        logger.debug "[MiniRecord] Adding column #{table_name}.#{column.name}" if logger
        connection.add_column table_name, column.name, column.type.to_sym, options unless dry_run
      end
    end

    # Add indexes
    indexes.each do |name, options|
      options = options.dup
      options.delete(:foreign)
      adjusted_index_name = "index_#{table_name}_on_" + (options[:column].is_a?(Array) ? options[:column].join('_and_') : options[:column]).to_s
      index_name = (options[:name] || adjusted_index_name).to_s
      unless connection.indexes(table_name).detect { |i| i.name == index_name }
        logger.debug "[MiniRecord] Adding index #{index_name} #{options[:column].inspect} on #{table_name}" if logger
        connection.add_index(table_name, options.delete(:column), options) unless dry_run
      end
    end

    add_foreign_keys(dry_run) if connection.respond_to?(:foreign_keys)

    # Reload column information
    reset_column_information
  end
end

#auto_upgrade_dryObject

dry-run



279
280
281
# File 'lib/mini_record/auto_schema.rb', line 279

def auto_upgrade_dry
  auto_upgrade!(true)
end

#clear_tables!(dry_run = false) ⇒ Object



186
187
188
189
190
191
192
193
194
195
# File 'lib/mini_record/auto_schema.rb', line 186

def clear_tables!(dry_run = false)
  return unless MiniRecord.configuration.destructive == true
  (connection.tables - schema_tables).each do |name|
    logger.debug "[MiniRecord] Dropping table #{name}" if logger
    unless dry_run
      connection.drop_table(name)
      schema_tables.delete(name)
    end
  end
end

#connection?Boolean

Returns:

  • (Boolean)


179
180
181
182
183
184
# File 'lib/mini_record/auto_schema.rb', line 179

def connection?
  !!connection
rescue Exception => e
  puts "\e[31m%s\e[0m" % e.message.strip
  false
end

#create_table(*options) ⇒ Object



162
163
164
# File 'lib/mini_record/auto_schema.rb', line 162

def create_table(*options)
  @create_table_options = options
end

#create_table_optionsObject



68
69
70
# File 'lib/mini_record/auto_schema.rb', line 68

def create_table_options
  @create_table_options ||= []
end

#field(*args) ⇒ Object Also known as: key, property, col



104
105
106
107
108
109
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
# File 'lib/mini_record/auto_schema.rb', line 104

def field(*args)
  return unless connection?

  options    = args.extract_options!
  type       = options.delete(:as) || options.delete(:type) || :string
  index      = options.delete(:index)

  args.each do |column_name|

    # Allow custom types like:
    #   t.column :type, "ENUM('EMPLOYEE','CLIENT','SUPERUSER','DEVELOPER')"
    if type.is_a?(String)
      # will be converted in: t.column :type, "ENUM('EMPLOYEE','CLIENT')"
      options.reverse_merge!(:limit => 0) unless postgresql_limitless_column?(type)
      table_definition.column(column_name, type, options)
    else
      # wil be converted in: t.string :name
      table_definition.send(type, column_name, options)
    end

    # Get the correct column_name i.e. in field :category, :as => :references
    column_name = table_definition.columns[-1].name

    # Parse indexes
    case index
    when Hash
      add_index(options.delete(:column) || column_name, index)
    when TrueClass
      add_index(column_name)
    when String, Symbol, Array
      add_index(index)
    end
  end
end

#field_attr_changes(field_name) ⇒ Object

Helper to determine if/how a field will change



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
# File 'lib/mini_record/auto_schema.rb', line 234

def field_attr_changes(field_name)
  field    = field_name.to_s
  changed  = false  # flag
  new_attr = {}

  # Next, iterate through our extended attributes, looking for any differences
  # This catches stuff like :null, :precision, etc
  # Ignore junk attributes that different versions of Rails include
  [:name, :limit, :precision, :scale, :default, :null].each do |att|
    value = fields[field][att]
    value = true if att == :null && value.nil?

    # Skip unspecified limit/precision/scale as DB will set them to defaults,
    # and on subsequent runs, this will be erroneously detected as a change.
    next if value.nil? and [:limit, :precision, :scale].include?(att)

    old_value = fields_in_db[field].send(att)
    # puts "#{field_name}[#{att}] = #{value.inspect} vs #{old_value.inspect}"

    attr_changed = false
    if att == :default
      # Rails 4.2 changed behavior to pass DB values directly through, so we must re-map
      if value.to_s =~ /^(false|f|0)$/i
        attr_changed = true if old_value.to_s !~ /^(false|f|0)$/i
      elsif value.to_s =~ /^(true|t|1)$/i
        attr_changed = true if old_value.to_s !~ /^(true|t|1)$/i
      elsif value.to_s != old_value.to_s
        attr_changed = true
      end
    elsif value != old_value
      attr_changed = true
    end

    if attr_changed
      logger.debug "[MiniRecord] Detected schema change for #{table_name}.#{field}##{att} " +
                   "from #{old_value.inspect} to #{value.inspect}" if logger
      new_attr[att] = value
      changed ||= attr_changed
    end
  end

  [new_attr, changed]
end

#fieldsObject



76
77
78
79
80
81
# File 'lib/mini_record/auto_schema.rb', line 76

def fields
  table_definition.columns.inject({}) do |hash, column|
    hash[column.name] = column
    hash
  end
end

#fields_in_dbObject



83
84
85
86
87
88
# File 'lib/mini_record/auto_schema.rb', line 83

def fields_in_db
  connection.columns(table_name).inject({}) do |hash, column|
    hash[column.name] = column
    hash
  end
end

#foreign_keysObject



197
198
199
200
201
# File 'lib/mini_record/auto_schema.rb', line 197

def foreign_keys
  # fk cache to minimize quantity of sql queries
  @foreign_keys ||= {}
  @foreign_keys[:table_name] ||= connection.foreign_keys(table_name)
end

#get_sql_field_type(field) ⇒ Object



60
61
62
63
64
65
66
# File 'lib/mini_record/auto_schema.rb', line 60

def get_sql_field_type(field)
  if ActiveRecord::VERSION::MAJOR.to_i < 4
    field.sql_type.to_s.downcase
  else
    connection.type_to_sql(field.type.to_sym, field.limit, field.precision, field.scale)
  end
end

#indexesObject



41
42
43
44
45
# File 'lib/mini_record/auto_schema.rb', line 41

def indexes
  return superclass.indexes unless (superclass == ActiveRecord::Base) || (superclass.respond_to?(:abstract_class?) && superclass.abstract_class?)

  @_indexes ||= {}
end

#indexes_in_dbObject



53
54
55
56
57
58
# File 'lib/mini_record/auto_schema.rb', line 53

def indexes_in_db
  connection.indexes(table_name).inject({}) do |hash, index|
    hash[index.name] = index
    hash
  end
end

#init_table_definition(connection) ⇒ Object



8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# File 'lib/mini_record/auto_schema.rb', line 8

def init_table_definition(connection)
  #connection.create_table(table_name) unless connection.table_exists?(table_name)

  case ActiveRecord::ConnectionAdapters::TableDefinition.instance_method(:initialize).arity
  when 1
    # Rails 3.2 and earlier
    ActiveRecord::ConnectionAdapters::TableDefinition.new(connection)
  when 4
    # Rails 4
    ActiveRecord::ConnectionAdapters::TableDefinition.new(connection.native_database_types, table_name, false, {})
  when -5
    # Rails 4.1
    ActiveRecord::ConnectionAdapters::TableDefinition.new(connection.native_database_types, table_name, false, {}, nil)
  else
    raise ArgumentError,
      "Unsupported number of args for ActiveRecord::ConnectionAdapters::TableDefinition.new()"
  end
end

#postgresql_limitless_column?(type) ⇒ Boolean

Returns:

  • (Boolean)


142
143
144
145
# File 'lib/mini_record/auto_schema.rb', line 142

def postgresql_limitless_column? type
  return unless connection.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
  type =~ /range|json/i
end

#remove_foreign_keys(dry_run) ⇒ Object

Remove foreign keys for indexes with :foreign=>false option



204
205
206
207
208
209
210
211
212
213
214
215
216
# File 'lib/mini_record/auto_schema.rb', line 204

def remove_foreign_keys(dry_run)
  return unless MiniRecord.configuration.destructive == true
  indexes.each do |name, options|
    if options[:foreign]==false
      foreign_key = foreign_keys.detect { |fk| fk.options[:column] == options[:column].to_s }
      if foreign_key
        logger.debug "[MiniRecord] Removing Foreign Key #{foreign_key.options[:name]} on table #{table_name}" if logger
        connection.remove_foreign_key(table_name, :name => foreign_key.options[:name]) unless dry_run
        foreign_keys.delete(foreign_key)
      end
    end
  end
end

#rename_field(*args) ⇒ Object Also known as: rename_key, rename_property, rename_col



90
91
92
93
94
95
96
97
98
99
# File 'lib/mini_record/auto_schema.rb', line 90

def rename_field(*args)
  return unless connection?

  options    = args.extract_options!
  new_name   = options.delete(:new_name)
  old_name   = args.first
  if old_name && new_name
    rename_fields[old_name] = new_name
  end
end

#rename_fieldsObject



72
73
74
# File 'lib/mini_record/auto_schema.rb', line 72

def rename_fields
  @rename_fields ||= {}
end

#reset_table_definition!Object Also known as: reset_schema!



151
152
153
# File 'lib/mini_record/auto_schema.rb', line 151

def reset_table_definition!
  @_table_definition = nil
end

#schema {|table_definition| ... } ⇒ Object

Yields:



156
157
158
159
160
# File 'lib/mini_record/auto_schema.rb', line 156

def schema
  reset_table_definition!
  yield table_definition
  table_definition
end

#schema_tablesObject



27
28
29
# File 'lib/mini_record/auto_schema.rb', line 27

def schema_tables
  @@_schema_tables ||= []
end

#suppress_index(*associations) ⇒ Object



173
174
175
176
177
# File 'lib/mini_record/auto_schema.rb', line 173

def suppress_index(*associations)
  associations.each do |association|
    suppressed_indexes[association] = true
  end
end

#suppressed_indexesObject



47
48
49
50
51
# File 'lib/mini_record/auto_schema.rb', line 47

def suppressed_indexes
  return superclass.suppressed_indexes unless (superclass == ActiveRecord::Base) || (superclass.respond_to?(:abstract_class?) && superclass.abstract_class?)

  @_suppressed_indexes ||= {}
end

#table_definitionObject



31
32
33
34
35
36
37
38
39
# File 'lib/mini_record/auto_schema.rb', line 31

def table_definition
  return superclass.table_definition unless (superclass == ActiveRecord::Base) || (superclass.respond_to?(:abstract_class?) && superclass.abstract_class?)

  @_table_definition ||= begin
    tb = init_table_definition(connection)
    tb.primary_key(primary_key)
    tb
  end
end

#timestampsObject



147
148
149
# File 'lib/mini_record/auto_schema.rb', line 147

def timestamps
  field :created_at, :updated_at, :as => :datetime
end