cover

Rules:

1. Don't

2. See rule 1!

3. See rule 1!

4. See rule 1!

5. See rule 1!

Seriously now


    class Something < ApplicationRecord
      alias_attribute :new_column_name, :existing_column_name
    end
  

But...

still..

The 5 Steps to Rename a Database Column with Zero Downtime

Step 1

Create the new column


    class Car < ApplicationRecord
      # Has a column named :kar_blueray_thor
    end
  

    class AddCarburatorToCars < ActiveRecord::Migration[7.1]
      def change
        add_column :cars, :carburator, :string
      end
    end
  

    bundle exec rails db:migrate
  

Performance implications

1. Make sure you don't have a default value for the new column.

2. Add indexes concurrently


    class AddIndexToCarburator < ActiveRecord::Migration[7.1]
      disable_ddl_transaction!

      def change
        add_index :cars, :carburator, algorithm: :concurrently
      end
    end
  

Step 2

Read from first, write in both


    class Car < ApplicationRecord
      # Read from first
      def carburator
        kar_blueray_thor
      end

      # Write in both
      def carburator=(value)
        self.update(carburator:       value)
        self.update(kar_blueray_thor: value)
      end
      alias_method :kar_blueray_thor=, :carburator=
    end
  

Update your codebase

1. Use your new methods everywhere in the codebase.

2. Make sure all tests pass.

3. Deploy your code.

4. Monitor for errors.

Step 3

Migrate your data

This is where the fun begins!

Create a rake task

Have the follwoing 3 arguments:

1. Batch Size

2. Batch Delay

3. Cursor

Make the task idempotent! Don't trigger callbacks!

The task itself


    namespace :data do
      task :carburator do
          size   = ENV.fetch('BATCH_SIZE').to_i
          delay  = ENV.fetch('BATCH_DELAY').to_i
          cursor = ENV.fetch('CURSOR').to_i

          CarburatorDataFill.new(size, delay, cursor).call
      end
    end
  

Filler service


    class CarburatorDataFill
      # initializer...

      def call
        Car.where(id: @cursor..).in_batches(of: @size) do |cars|
          cars.each do |car|
            car.carburator = car.kar_blueray_thor
            car.save!
          end

          sleep @delay
        end
      end
    end
  

Transactions

If your models trigger extra queries on this operation, wrap them in a transaction block:

    cars.each do |car|
      ApplicationRecord.transaction do
        car.carburator = car.kar_blueray_thor
        car.save!
      end
    end
  

Ideally there are no such callbacks!

Run the task in a production shell

1. Deploy your code.

2. Start a shell and execute the task with LOG_LEVEL=debug!

3. Monitor for errors and performance degradation.

Stop execution if you notice issues, and update the 3 parameters for your next run!

Crazy idea that might just work

You could deploy and execute your task in a background job if you can't have shell access.

Only if you have quick and easy access to stop and tweak the parameteres!

And always monitor the performance metrics, errors, and logs!

Step 4

Read from second, write in both


    class Car < ApplicationRecord
      # Read from second
      def kar_blueray_thor
        carburator
      end

      # Write in both
      def kar_blueray_thor=(value)
        self.update(carburator:       value)
        self.update(kar_blueray_thor: value)
      end
      alias_method :carburator=, :kar_blueray_thor=
    end
  

Deploy and enjoy

1. Make sure all tests still pass.

2. Deploy your code.

3. Monitor for errors.

Step 5

Cleanup


    class Car < ApplicationRecord
      # Old code removed!
    end
  

    -- Make sure the column is REALLY no longer used!
    REVOKE ALL PRIVILEGES
    ON cars(kar_blueray_thor)
    FROM app_database_user;
  

    class RemoveKarThingyFromCars < ActiveRecord::Migration[7.1]
      # This is IRREVERSIBLE!
      def up
        remove_column :cars, :kar_blueray_thor
      end
    end
  

Technically speaking, it is reversible


    class RemoveKarThingyFromCars < ActiveRecord::Migration[7.1]
      def up
        remove_column :cars, :kar_blueray_thor
      end

      def down
        add_column :cars, :kar_blueray_thor, :string
      end
    end
  

And you can restore the data from a backup...

...Profit?

Advantages:

1. Zero Downtime!

2. Reversible every step of the way

3. Slow, you have time to catch bugs

Disadvantages:

1. Slow

2. Works best with continuous deployments

You can probably get away with


    class Something < ApplicationRecord
      alias_attribute :new_column_name, :existing_column_name
    end
  

But it's good to know the entire process, maybe you will need parts of it for other Zero Downtime workloads.

Thank You!

Q&A