module DbSchema::Changes

Public Class Methods

between(desired_schema, actual_schema) click to toggle source
# File lib/db_schema/changes.rb, line 7
def between(desired_schema, actual_schema)
  sort_all_changes(
    [
      table_changes(desired_schema, actual_schema),
      enum_changes(desired_schema, actual_schema),
      extension_changes(desired_schema, actual_schema)
    ].reduce(:+)
  )
end

Private Class Methods

check_changes(desired_table, actual_table) click to toggle source
# File lib/db_schema/changes.rb, line 107
def check_changes(desired_table, actual_table)
  compare_collections(
    desired_table.checks,
    actual_table.checks,
    create: -> (check) { Operations::CreateCheckConstraint.new(check) },
    drop:   -> (check) { Operations::DropCheckConstraint.new(check.name) },
    change: -> (desired, actual) do
      [
        Operations::DropCheckConstraint.new(actual.name),
        Operations::CreateCheckConstraint.new(desired)
      ]
    end
  )
end
compare_collections(desired, actual, create:, drop:, change: -> (*) {} click to toggle source
# File lib/db_schema/changes.rb, line 179
def compare_collections(desired, actual, create:, drop:, change: -> (*) {})
  desired_hash = Utils.to_hash(desired, :name)
  actual_hash  = Utils.to_hash(actual, :name)

  (desired_hash.keys + actual_hash.keys).uniq.flat_map do |name|
    if desired_hash.key?(name) && !actual_hash.key?(name)
      create.(desired_hash[name])
    elsif actual_hash.key?(name) && !desired_hash.key?(name)
      drop.(actual_hash[name])
    elsif actual_hash[name] != desired_hash[name]
      change.(desired_hash[name], actual_hash[name])
    end
  end.compact
end
enum_changes(desired_schema, actual_schema) click to toggle source
# File lib/db_schema/changes.rb, line 137
def enum_changes(desired_schema, actual_schema)
  compare_collections(
    desired_schema.enums,
    actual_schema.enums,
    create: -> (enum) { Operations::CreateEnum.new(enum) },
    drop:   -> (enum) { Operations::DropEnum.new(enum.name) },
    change: -> (desired, actual) do
      fields = actual_schema.tables.flat_map do |table|
        table.fields.select do |field|
          if field.array?
            field.attributes[:element_type] == actual.name
          else
            field.type == actual.name
          end
        end.map do |field|
          if desired_field = desired_schema[table.name][field.name]
            new_default = desired_field.default
          end

          {
            table_name:  table.name,
            field_name:  field.name,
            new_default: new_default,
            array:       field.array?
          }
        end
      end

      Operations::AlterEnumValues.new(actual.name, desired.values, fields)
    end
  )
end
extension_changes(desired_schema, actual_schema) click to toggle source
# File lib/db_schema/changes.rb, line 170
def extension_changes(desired_schema, actual_schema)
  compare_collections(
    desired_schema.extensions,
    actual_schema.extensions,
    create: -> (extension) { Operations::CreateExtension.new(extension) },
    drop:   -> (extension) { Operations::DropExtension.new(extension.name) }
  )
end
field_changes(desired_table, actual_table) click to toggle source
# File lib/db_schema/changes.rb, line 59
def field_changes(desired_table, actual_table)
  compare_collections(
    desired_table.fields,
    actual_table.fields,
    create: -> (field) { Operations::CreateColumn.new(field) },
    drop:   -> (field) { Operations::DropColumn.new(field.name) },
    change: -> (desired, actual) do
      [].tap do |operations|
        if (actual.type != desired.type) || (actual.attributes != desired.attributes)
          operations << Operations::AlterColumnType.new(
            actual.name,
            old_type: actual.type,
            new_type: desired.type,
            **desired.attributes
          )
        end

        if desired.null? && !actual.null?
          operations << Operations::AllowNull.new(actual.name)
        end

        if actual.null? && !desired.null?
          operations << Operations::DisallowNull.new(actual.name)
        end

        if actual.default != desired.default
          operations << Operations::AlterColumnDefault.new(actual.name, new_default: desired.default)
        end
      end
    end
  )
end
foreign_key_changes(desired_table, actual_table) click to toggle source
# File lib/db_schema/changes.rb, line 122
def foreign_key_changes(desired_table, actual_table)
  compare_collections(
    desired_table.foreign_keys,
    actual_table.foreign_keys,
    create: -> (foreign_key) { Operations::CreateForeignKey.new(actual_table.name, foreign_key) },
    drop:   -> (foreign_key) { Operations::DropForeignKey.new(actual_table.name, foreign_key.name) },
    change: -> (desired, actual) do
      [
        Operations::DropForeignKey.new(actual_table.name, actual.name),
        Operations::CreateForeignKey.new(actual_table.name, desired)
      ]
    end
  )
end
index_changes(desired_table, actual_table) click to toggle source
# File lib/db_schema/changes.rb, line 92
def index_changes(desired_table, actual_table)
  compare_collections(
    desired_table.indexes,
    actual_table.indexes,
    create: -> (index) { Operations::CreateIndex.new(index) },
    drop:   -> (index) { Operations::DropIndex.new(index.name, index.primary?) },
    change: -> (desired, actual) do
      [
        Operations::DropIndex.new(actual.name, actual.primary?),
        Operations::CreateIndex.new(desired)
      ]
    end
  )
end
sort_all_changes(changes) click to toggle source
# File lib/db_schema/changes.rb, line 194
def sort_all_changes(changes)
  Utils.sort_by_class(
    changes,
    [
      Operations::CreateExtension,
      Operations::DropForeignKey,
      Operations::AlterEnumValues,
      Operations::CreateEnum,
      Operations::CreateTable,
      Operations::AlterTable,
      Operations::DropTable,
      Operations::DropEnum,
      Operations::CreateForeignKey,
      Operations::DropExtension
    ]
  )
end
sort_alter_table_changes(changes) click to toggle source
# File lib/db_schema/changes.rb, line 212
def sort_alter_table_changes(changes)
  Utils.sort_by_class(
    changes,
    [
      Operations::DropCheckConstraint,
      Operations::DropIndex,
      Operations::DropColumn,
      Operations::AlterColumnType,
      Operations::AllowNull,
      Operations::DisallowNull,
      Operations::AlterColumnDefault,
      Operations::CreateColumn,
      Operations::CreateIndex,
      Operations::CreateCheckConstraint
    ]
  )
end
table_changes(desired_schema, actual_schema) click to toggle source
# File lib/db_schema/changes.rb, line 18
def table_changes(desired_schema, actual_schema)
  compare_collections(
    desired_schema.tables,
    actual_schema.tables,
    create: -> (table) do
      fkey_operations = table.foreign_keys.map do |fkey|
        Operations::CreateForeignKey.new(table.name, fkey)
      end

      [Operations::CreateTable.new(table), *fkey_operations]
    end,
    drop: -> (table) do
      fkey_operations = table.foreign_keys.map do |fkey|
        Operations::DropForeignKey.new(table.name, fkey.name)
      end

      [Operations::DropTable.new(table.name), *fkey_operations]
    end,
    change: -> (desired, actual) do
      fkey_operations = foreign_key_changes(desired, actual)

      alter_table_operations = [
        field_changes(desired, actual),
        index_changes(desired, actual),
        check_changes(desired, actual)
      ].reduce(:+)

      if alter_table_operations.any?
        alter_table = Operations::AlterTable.new(
          desired.name,
          sort_alter_table_changes(alter_table_operations)
        )

        [alter_table, *fkey_operations]
      else
        fkey_operations
      end
    end
  )
end