class EZ::SchemaModifier

The SchemaModifier class receives a DomainModeler specification and applies any necessary db schema changes.

Attributes

db[R]
dry_run[R]
spec[R]

Public Class Methods

migrate(model_spec, silent = false, dry_run = false) click to toggle source
# File lib/ez/schema_modifier.rb, line 16
def self.migrate(model_spec, silent = false, dry_run = false)
  self.new(model_spec, silent, dry_run).migrate
end
new(model_spec, silent = false, dry_run = false) click to toggle source
# File lib/ez/schema_modifier.rb, line 9
def initialize(model_spec, silent = false, dry_run = false)
  @silent = silent
  @dry_run = dry_run
  @spec = model_spec
  connect_to_database
end

Public Instance Methods

add_missing_columns(model_name, spec_columns, assume_missing = false) click to toggle source
# File lib/ez/schema_modifier.rb, line 76
def add_missing_columns(model_name, spec_columns, assume_missing = false)
  table_name = model_name.tableize
  db_columns = db.columns(table_name)

  spec_columns.each do |col_name, col_spec|
    col_type = col_spec[:type].to_sym
    col_default = col_spec[:default]
    db_col = !assume_missing && (db_columns.detect { |dbc| dbc.name == col_name })

    if !db_col
      if !assume_missing
        display_change "Adding new column '#{col_name}' as #{col_type} in model #{model_name}"
      end
      opts = { default: col_default }
      opts[:limit] = col_spec[:limit] if col_spec[:limit]
      db.add_column(table_name, col_name.to_sym, col_type.to_sym, opts)  unless @dry_run
      if col_spec[:index]
        display_change "  (adding database index for '#{col_name}')"
        db.add_index table_name, col_name.to_sym unless @dry_run
      end
    else
      if db_col.type != col_type
        display_change "Changing column type for #{col_name} to #{col_type} in model #{model_name}"
      end

      # puts "#{table_name} #{col_name}: #{db_col.default} and #{col_default}"
      if col_type == :boolean
        db_col_default = db_col.default.in?(EZ::COLUMN_TRUE_VALUES)
      end

      if db_col_default != col_default
        displayable_value = col_default
        displayable_value = "NULL" if col_default.nil?
        display_change "Applying new default value `#{displayable_value}` for #{col_name} attrbute in model #{model_name}"
      end

      if (db_col.type != col_type) || (db_col.default != col_default)
          opts = { default: col_default }
          opts[:limit] = col_spec[:limit] if col_spec[:limit]
          db.change_column(table_name, col_name.to_sym, col_type.to_sym, opts)  unless @dry_run
      end
    end
  end
end
add_missing_schema() click to toggle source
# File lib/ez/schema_modifier.rb, line 61
def add_missing_schema
  @spec.each do |model_name, columns|
    if missing_model?(model_name)
      add_model(model_name, columns)
    else
      add_missing_columns(model_name, columns)
    end
  end
end
add_model(model_name, spec_columns) click to toggle source
# File lib/ez/schema_modifier.rb, line 121
def add_model(model_name, spec_columns)
  table_name = model_name.tableize
  display_change "Defining new table for model '#{model_name}'."
  db.create_table table_name  unless @dry_run
  add_missing_columns model_name, spec_columns, true
  filename = "app/models/#{model_name.underscore}.rb"
  if (Rails.env.development? || Rails.env.test?) && !File.exists?(filename)
    display_change "Creating new model file: #{filename}"
    File.open(filename, "w") do |f|
      base_class = "ApplicationRecord"
      base_class = "ActiveRecord::Base" if Rails::VERSION::MAJOR < 5
      f.puts "class #{model_name} < #{base_class}"
      f.puts "end"
    end
  end
end
connect_to_database() click to toggle source
# File lib/ez/schema_modifier.rb, line 200
def connect_to_database
  ActiveRecord::Base.establish_connection
  @db = ActiveRecord::Base.connection
end
display_change(message) click to toggle source
# File lib/ez/schema_modifier.rb, line 71
def display_change(message)
  puts message unless @silent
  @changed = true
end
migrate() click to toggle source
# File lib/ez/schema_modifier.rb, line 20
def migrate
  @changed = false

  if @dry_run
    puts "Previewing... no changes will actually take place..."
    puts
  end

  add_missing_schema
  remove_dead_schema

  if @changed
    puts "\n(No changes were actually made)" if @dry_run
  else
      # puts "All tables are up-to-date."
    if @dry_run
      puts "\nNo changes would be made."
    end
  end

  return @changed

  rescue => e
    puts e.message
    puts e.backtrace.first
    false
end
missing_model?(model_name) click to toggle source
# File lib/ez/schema_modifier.rb, line 53
def missing_model?(model_name)
  missing_table?(model_name.tableize)
end
missing_table?(name) click to toggle source
# File lib/ez/schema_modifier.rb, line 57
def missing_table?(name)
  tables.index(name).nil?
end
remove_dead_columns() click to toggle source
# File lib/ez/schema_modifier.rb, line 146
def remove_dead_columns
  return unless Rails.env.development? || Rails.env.test?

  tables.each do |table_name|
    model_name = table_name.classify.to_s

    if @spec.has_key?(model_name)
      db_columns = db.columns(table_name).map { |column| column.name.to_sym } - [:id, :created_at, :updated_at]
      spec_columns = @spec[model_name].keys.map(&:to_sym)
      # puts spec_columns.inspect
      dead_columns = db_columns - spec_columns


      if dead_columns.any?
        dead_columns.each do |dead_column_name|
          display_change "Removing unused column '#{dead_column_name}' from model '#{model_name}'"
        end
        db.remove_columns(table_name, *dead_columns)  unless @dry_run
      end
    end
  end
end
remove_dead_schema() click to toggle source
# File lib/ez/schema_modifier.rb, line 138
def remove_dead_schema
  return unless Rails.env.development? || Rails.env.test?
  return if Dir[File.join(Rails.root, 'db/migrate/*.rb')].entries.any?

  remove_dead_tables
  remove_dead_columns
end
remove_dead_tables() click to toggle source
# File lib/ez/schema_modifier.rb, line 174
def remove_dead_tables
  return unless Rails.env.development? || Rails.env.test?
  return if Dir[File.join(Rails.root, 'db/migrations/*.rb')].entries.any?

  tables_we_need = @spec.keys.map { |model| model.tableize }
  dead_tables = tables - tables_we_need

  dead_tables.each do |table_name|
    model_name = table_name.classify
    display_change "Dropping table #{table_name}"
    db.drop_table(table_name) unless @dry_run
    begin
      filename = "app/models/#{model_name.underscore}.rb"
      code = IO.read(filename)
      is_empty = IO.read(filename) =~ /\s*class #{model_name} < ApplicationRecord\s+end\s*/

      if is_empty
        display_change "Deleting file #{filename}"
        File.unlink(filename) unless @dry_run
      end
    rescue => e
      display_change "Could not delete old model #{model_name.underscore}.rb."
    end
  end
end
tables() click to toggle source
# File lib/ez/schema_modifier.rb, line 49
def tables
  @tables ||= (db.data_sources - ['schema_migrations', 'ar_internal_metadata'])
end
update_schema_version() click to toggle source
# File lib/ez/schema_modifier.rb, line 169
def update_schema_version
  # db.initialize_schema_migrations_table
  # db.assume_migrated_upto_version(Time.now.utc.strftime("%Y%m%d%H%M%S"))
end