Dekiru::DataMigration

A Ruby on Rails library for executing data migration tasks safely and efficiently.

Overview

Dekiru::DataMigration provides the following features for data migration tasks:

  • Progress Display: Real-time progress visualization during processing
  • Transaction Management: Automatic transaction control to ensure data safety
  • Execution Confirmation: Confirmation prompts before committing changes
  • Side Effect Monitoring: Tracking of database queries, job enqueuing, and email sending
  • Logging: Detailed execution logging

Installation

Add this line to your application's Gemfile:

gem 'dekiru-data_migration'

And then execute:

$ bundle install

Or install it yourself as:

$ gem install dekiru-data_migration

Data Migration Class

Define migration logic as a class:

# scripts/20230118_demo_migration.rb
class DemoMigration < Dekiru::DataMigration::Migration
  def migration_targets
    User.where("email LIKE '%sonicgarden%'").where(admin: false)
  end

  def migrate_record(user)
    user.update!(admin: true)
  end

  def migrate
    super
    log "Updated user count: #{User.where(admin: true).count}"
  end
end

DemoMigration.run

Batch Processing with migrate_batch

When you need to update or delete a large number of records efficiently, you can define migrate_batch instead of migrate_record. It processes records in batches using ActiveRecord's in_batches, yielding each batch as an ActiveRecord::Relation so you can run a single update_all / delete_all query per batch. The progress bar advances per batch rather than per record.

migrate_batch and migrate_record are mutually exclusive — if you implement migrate_batch, the batch path is used automatically.

# scripts/20230118_deactivate_stale_users.rb
class DeactivateStaleUsersMigration < Dekiru::DataMigration::Migration
  def migration_targets
    User.where(active: true).where("last_login_at < ?", 1.year.ago)
  end

  def migrate_batch(relation)
    relation.update_all(active: false)
  end
end

DeactivateStaleUsersMigration.run

Specifying the batch size

Pass batch_size to run to control how many records are fetched per batch. It applies to both paths — in_batches(of:) for migrate_batch and find_each(batch_size:) for migrate_record. When omitted, ActiveRecord's default of 1000 is used.

DeactivateStaleUsersMigration.run(batch_size: 500)

Testing Migration Classes

The class-based approach makes it easy to write unit tests:

# spec/migrations/demo_migration_spec.rb
RSpec.describe DemoMigration do
  let(:migration) { described_class.new }

  describe '#migration_targets' do
    it 'returns correct migration targets' do
      create_list(:user, 3, email: 'test@sonicgarden.jp', admin: false)
      create_list(:user, 2, email: 'other@example.com', admin: false)

      targets = migration.migration_targets
      expect(targets.count).to eq(3)
      expect(targets.all? { |u| u.email.include?('sonicgarden') }).to be true
    end
  end

  describe '#migrate_record' do
    it 'updates user to admin' do
      user = create(:user, admin: false)
      expect { migration.migrate_record(user) }
        .to change { user.reload.admin }.from(false).to(true)
    end
  end
end

Execution result:

$ bin/rails r scripts/demo.rb
Start: Grant admin privileges to users at 2019-05-24 18:29:57 +0900

Target user count: 30
Time: 00:00:00 |=================>>| 100% Progress
Updated user count: 30

Are you sure to commit? (yes/no) > yes

Finished successfully: Grant admin privileges to users
Total time: 6.35 sec

Side Effect Monitoring

By passing warning_side_effects: true to run, side effects that occur during data migration tasks (database writes, job enqueuing, email sending, etc.) will be displayed.

DemoMigration.run(warning_side_effects: true)

Execution result:

$ bin/rails r scripts/demo.rb
Start: Grant admin privileges to users at 2019-05-24 18:29:57 +0900

Target user count: 30
Time: 00:00:00 |=================>>| 100% Progress
Updated user count: 30

Write Queries!!
30 call: Update "users" SET ...

Enqueued Jobs!!
10 call: NotifyJob

Delivered Mailers!!
10 call: UserMailer

Are you sure to commit? (yes/no) > yes

Generating Maintenance Scripts

You can generate maintenance scripts that use Dekiru::DataMigration::Migration with the generator. The filename will be prefixed with the execution date.

$ bin/rails g maintenance_script demo_migration

Generated file example:

# scripts/20230118_demo_migration.rb
# frozen_string_literal: true

class DemoMigration < Dekiru::DataMigration::Migration
  def migration_targets
    # Return an ActiveRecord::Relation of records to migrate
    # e.g. User.where(some_condition: true)
    raise NotImplementedError, 'migration_targets method must be implemented'
  end

  def migrate_record(record)
    # Define the update logic for each record
    # e.g. record.update!(some_attribute: new_value)
    raise NotImplementedError, 'migrate_record method must be implemented'
  end
end

DemoMigration.run

Output Directory Configuration

The output directory for files is by default the scripts directory directly under the application root. You can change the output directory through configuration.

# config/initializers/dekiru.rb
Dekiru::DataMigration.configure do |config|
  config.maintenance_script_directory = 'scripts/maintenance'
end

Custom Transaction Management

For scripts using Dekiru::DataMigration::Operator, there are cases where the default ActiveRecord::Base.transaction transaction handling is insufficient, such as when writing to multiple databases is required.

You can modify the transaction handling behavior of Dekiru::DataMigration::Operator by customizing Dekiru::DataMigration::TransactionProvider.

Implementation Example

Here's an example configuration for applications using multiple databases.

Application-side Configuration

# app/models/legacy_record.rb
class LegacyRecord < ApplicationRecord
  connects_to database: { writing: :legacy, reading: :legacy }
end

# app/models/application_record.rb
class ApplicationRecord < ActiveRecord::Base
  connects_to database: { writing: :primary, reading: :primary }

  def self.with_legacy_transaction
    ActiveRecord::Base.transaction do
      LegacyRecord.transaction do
        yield
      end
    end
  end
end

Custom TransactionProvider Configuration

To configure Dekiru::DataMigration::Operator to also use ApplicationRecord.with_legacy_transaction for transaction handling, set up the following configuration:

# config/initializers/dekiru.rb
class MyTransactionProvider < Dekiru::DataMigration::TransactionProvider
  def within_transaction(&)
    ApplicationRecord.with_legacy_transaction(&)
  end
end

Dekiru::DataMigration.configure do |config|
  config.transaction_provider = MyTransactionProvider.new
end

Available Configuration Options

Basic Configuration

# config/initializers/dekiru.rb
Dekiru::DataMigration.configure do |config|
  # Output directory for maintenance scripts (default: "scripts")
  config.maintenance_script_directory = 'scripts/maintenance'

  # Custom transaction provider (default: Dekiru::DataMigration::TransactionProvider.new)
  config.transaction_provider = MyTransactionProvider.new
end

Runtime Options

Pass options to Migration.run:

DemoMigration.run(warning_side_effects: true, without_transaction: false)

Available options:

  • warning_side_effects: Display side effects (default: true)
  • without_transaction: Don't use transactions (default: false)
  • logger: Custom logger (default: auto-generated)
  • output: Output destination (default: $stdout)

Key Methods

log(message)

Outputs log messages. Properly handled even during progress bar display.

find_each_with_progress(scope, options = {}, &block)

Executes find_each with a progress bar for ActiveRecord scopes. Pass batch_size: in options to control how many records are fetched per batch (forwarded to find_each(batch_size:)).

each_with_progress(enum, options = {}, &block)

Executes processing with a progress bar for any Enumerable objects.

Agent Skills

This repository provides an agent skill for creating data migration scripts.

Available Skills

data-migration-script

Automatically creates data migration and deletion scripts using the dekiru-data_migration gem.

Triggers: The agent will use this skill when you ask to "create a data migration script", "create a script to delete unnecessary records", or similar requests involving DB operations via scripts (bulk updates, deleting orphaned records, deleting ActiveStorage files, etc.).

Install:

gh skill install SonicGarden/dekiru-data_migration