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

Reversible Migration to flipper-active_record #7

Open
olivierlacan opened this issue Jun 29, 2017 · 1 comment
Open

Reversible Migration to flipper-active_record #7

olivierlacan opened this issue Jun 29, 2017 · 1 comment

Comments

@olivierlacan
Copy link

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.

@olivierlacan
Copy link
Author

Or I guess, maybe I should leave this open for visibility?

@olivierlacan olivierlacan reopened this Jun 29, 2017
# for free to subscribe to this conversation on GitHub. Already have an account? #.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant