Skip to content
This repository was archived by the owner on Jan 21, 2023. It is now read-only.
This repository was archived by the owner on Jan 21, 2023. It is now read-only.

Reversible Migration to flipper-active_record #7

Description

@olivierlacan

Since I had a production application depending on this gem I wrote a large migration to move
from it to the new official flipper-active_record adapter gem. Posted here to help anyone who might face a similar struggle. This is a fully reversible migration.

I'm assuming you used the following original migration for flipper-activerecord:

class CreateFlipperTables < ActiveRecord::Migration
  def self.up
    create_table :flipper_features do |t|
      t.string :name, null: false
      t.timestamps null: false
    end
    add_index :flipper_features, :name, unique: true

    create_table :flipper_gates do |t|
      t.integer :flipper_feature_id, null: false
      t.string :name, null: false
      t.string :value
      t.timestamps null: false
    end
    add_foreign_key :flipper_gates, :flipper_features, on_delete: :cascade
    add_index :flipper_gates, [:flipper_feature_id, :name, :value], unique: true
  end

  def self.down
    remove_foreign_key :flipper_gates, :flipper_features
    drop_table :flipper_gates
    drop_table :flipper_features
  end
end

Again, I'm assuming you are moving to flipper-active_record which at the time of this writing uses the following migration:

class CreateFlipperTables < ActiveRecord::Migration
  def self.up
    create_table :flipper_features do |t|
      t.string :key, null: false
      t.timestamps null: false
    end
    add_index :flipper_features, :key, unique: true

    create_table :flipper_gates do |t|
      t.string :feature_key, null: false
      t.string :key, null: false
      t.string :value
      t.timestamps null: false
    end
    add_index :flipper_gates, [:feature_key, :key, :value], unique: true
  end

  def self.down
    drop_table :flipper_gates
    drop_table :flipper_features
  end
end

The two migrations are similar aside from the following changes necessary to be compatible with flipper-active_record:

  • flipper_features table uses key string over name string to identify the feature
  • flipper_gates table uses feature_key string over flipper_feature_id integer to reference the related flipper_features record
  • flipper_gates table uses key string over name string to identify the gate
  • there is no foreign key associating flipper_features and flipper_gates

Since I'm assuming that — like me — you had both existing flipper_features and flipper_gates records in production which you could not afford to wipe, here's the migration:

class MigrateToFlipperActiveRecord < ActiveRecord::Migration
  # Create Migration classes to avoid relying on models that may not
  # exist in the future. See:
  # http://blog.testdouble.com/posts/2014-11-04-healthy-migration-habits
  class MigrationFeature < ActiveRecord::Base
    self.table_name = "flipper_features"
  end
  class MigrationGate < ActiveRecord::Base
    self.table_name = "flipper_gates"
  end

  def up
    # Add new columns and indices
    add_column :flipper_features, :key, :string

    # Migrate existing data to avoid errors when enabling null: false
    MigrationFeature.all.find_each do |feature|
      feature.update_attribute(:key, feature.name)
    end

    change_column_null :flipper_features, :key, false

    add_column :flipper_gates, :key, :string
    add_column :flipper_gates, :feature_key, :string

    # Migrate existing data to avoid errors when enabling null: false
    MigrationGate.all.find_each do |gate|
      feature = MigrationFeature.find_by(id: gate.flipper_feature_id)
      gate.update(key: gate.name, feature_key: feature.key)
    end

    change_column_null :flipper_gates, :key, false
    change_column_null :flipper_gates, :feature_key, false

    add_index :flipper_features, :key, unique: true

    add_index :flipper_gates, [:feature_key, :key], unique: true

    # Renove old columns (and indices automatically)
    remove_column :flipper_features, :name

    change_table :flipper_gates do |t|
      t.remove :flipper_feature_id
      t.remove :name
    end
  end

  def down
    # Add old columns and indices
    add_column :flipper_features, :name, :string

    # Migrate existing data to avoid errors when enabling null: false
    MigrationFeature.all.find_each do |feature|
      feature.update_attribute(:name, feature.key)
    end

    change_column_null :flipper_features, :name, false

    add_index :flipper_features, :name, unique: true

    add_column :flipper_gates, :flipper_feature_id, :integer
    add_column :flipper_gates, :name, :string

    # Migrate existing data to avoid errors when enabling null: false
    MigrationGate.all.find_each do |gate|
      feature = MigrationFeature.find_by(key: gate.feature_key)
      gate.update(
        flipper_feature_id: feature.id,
        name: gate.key
      )
    end

    change_column_null :flipper_gates, :flipper_feature_id, false
    change_column_null :flipper_gates, :name, false

    add_foreign_key :flipper_gates, :flipper_features,
      on_delete: :cascade
    add_index :flipper_gates, [:flipper_feature_id, :name],
      unique: true

    # Remove new columns (and indices automatically)
    remove_column :flipper_features, :key

    change_table :flipper_gates do |t|
      t.remove :feature_key
      t.remove :key
    end
  end
end

I've run this migration in both directions and checked state so it should be safe but I highly recommend testing in your development and staging environments before running it in production.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions