Generate background migration modules for creating, dropping, or renaming database indexes on large tables using the Explorer.Migrator.HeavyDbIndexOperation framework. Automatically updates the BackgroundMigrations cache module with proper tracking. These migrations run in the background with progress tracking and dependency management. Use this skill for requests on creating background migrations to delete / create / rename indexes on large tables (logs, internal_transactions, token_transfers, addresses, transactions, blocks, etc.) to avoid blocking the database.
The heavy-db-index-operation skill helps you generate migration modules that create, drop, or rename database indexes on large tables in a controlled, non-blocking manner. These operations use the Explorer.Migrator.HeavyDbIndexOperation behavior and are tracked via Explorer.Migrator.MigrationStatus.
What this skill generates:
apps/explorer/lib/explorer/migrator/heavy_db_index_operation/apps/explorer/lib/explorer/chain/cache/background_migrations.ex:
Each heavy index operation module must implement the Explorer.Migrator.HeavyDbIndexOperation behavior with these callbacks:
migration_name/0 - Automatically generated from operation_type and index_name. Format: heavy_indexes_{operation_type}_{lowercase_index_name}table_name/0 - Returns the table atom (:logs, :internal_transactions, :addresses, etc.)operation_type/0 - Returns :create, :drop, or :renameindex_name/0 - Returns the index name as a string (for renames, return the final/new index name)dependent_from_migrations/0 - Returns list of migration names this depends on (or [])db_index_operation/0 - Executes the actual index creation/deletion/renamecheck_db_index_operation_progress/0 - Checks operation progressdb_index_operation_status/0 - Returns operation statusrestart_db_index_operation/0 - Restarts the operation if neededrunning_other_heavy_migration_exists?/1 - Checks for conflicting migrationsupdate_cache/0 - Updates the BackgroundMigrations cache when migration completes@table_columns (Simple Indexes)Use this for straightforward indexes on one or more columns:
@table_columns ["address_hash", "block_number DESC", "index DESC"]
@impl HeavyDbIndexOperation
def db_index_operation do
HeavyDbIndexOperationHelper.create_db_index(@index_name, @table_name, @table_columns)
end
When to use:
@query_string (Complex Indexes)Use this for more complex index definitions:
@query_string """
CREATE INDEX #{HeavyDbIndexOperationHelper.add_concurrently_flag?()} IF NOT EXISTS "#{@index_name}"
ON #{@table_name} ((1))
WHERE verified = true;
"""
@impl HeavyDbIndexOperation
def db_index_operation do
HeavyDbIndexOperationHelper.create_db_index(@query_string)
end
When to use:
((1)) for existence check)Creation: CreateTableNameColumnNameIndex
CreateLogsAddressHashBlockNumberDescIndexDescIndexCreateAddressesVerifiedIndexDeletion: DropTableNameIndexName
DropInternalTransactionsCreatedContractAddressHashPartialIndexDropLogsAddressHashIndexRenaming: RenameOldIndexNameToNewIndexName or RenameTableNameIndexDescriptor
RenameTransactions2ndCreatedContractAddressHashWithPendingIndexAtable_name_column_name_suffix_index_partial in the name_desc in the namelogs_address_hash_block_number_DESC_index_DESC_indexaddresses_verified_indexinternal_transactions_created_contract_address_hash_partial_indexCreateLogsAddressHashIndex → create_logs_address_hash_index.exdependent_from_migrations/0Specify migrations that must complete before this one runs:
# No dependencies
@impl HeavyDbIndexOperation
def dependent_from_migrations, do: []
# Depends on another migration
alias Explorer.Migrator.EmptyInternalTransactionsData
@impl HeavyDbIndexOperation
def dependent_from_migrations do
[EmptyInternalTransactionsData.migration_name()]
end
# Multiple dependencies
alias Explorer.Migrator.HeavyDbIndexOperation.{
DropLogsIndexIndex,
DropLogsBlockNumberAscIndexAscIndex
}
@impl HeavyDbIndexOperation
def dependent_from_migrations do
[
DropLogsIndexIndex.migration_name(),
DropLogsBlockNumberAscIndexAscIndex.migration_name()
]
end
defmodule Explorer.Migrator.HeavyDbIndexOperation.CreateLogsAddressHashBlockNumberDescIndexDescIndex do
@moduledoc """
Create B-tree index `logs_address_hash_block_number_DESC_index_DESC_index` on `logs` table
for (`address_hash`, `block_number DESC`, `index DESC`) columns.
"""
use Explorer.Migrator.HeavyDbIndexOperation
require Logger
alias Explorer.Chain.Cache.BackgroundMigrations
alias Explorer.Migrator.{HeavyDbIndexOperation, MigrationStatus}
alias Explorer.Migrator.HeavyDbIndexOperation.Helper, as: HeavyDbIndexOperationHelper
@table_name :logs
@index_name "logs_address_hash_block_number_DESC_index_DESC_index"
@operation_type :create
@table_columns ["address_hash", "block_number DESC", "index DESC"]
@impl HeavyDbIndexOperation
def table_name, do: @table_name
@impl HeavyDbIndexOperation
def operation_type, do: @operation_type
@impl HeavyDbIndexOperation
def index_name, do: @index_name
@impl HeavyDbIndexOperation
def dependent_from_migrations, do: []
@impl HeavyDbIndexOperation
def db_index_operation do
HeavyDbIndexOperationHelper.create_db_index(@index_name, @table_name, @table_columns)
end
@impl HeavyDbIndexOperation
def check_db_index_operation_progress do
operation = HeavyDbIndexOperationHelper.create_index_query_string(@index_name, @table_name, @table_columns)
HeavyDbIndexOperationHelper.check_db_index_operation_progress(@index_name, operation)
end
@impl HeavyDbIndexOperation
def db_index_operation_status do
HeavyDbIndexOperationHelper.db_index_creation_status(@index_name)
end
@impl HeavyDbIndexOperation
def restart_db_index_operation do
HeavyDbIndexOperationHelper.safely_drop_db_index(@index_name)
end
@impl HeavyDbIndexOperation
def running_other_heavy_migration_exists?(migration_name) do
MigrationStatus.running_other_heavy_migration_for_table_exists?(@table_name, migration_name)
end
@impl HeavyDbIndexOperation
def update_cache do
BackgroundMigrations.set_heavy_indexes_create_logs_address_hash_block_number_desc_index_desc_index_finished(
true
)
end
end
defmodule Explorer.Migrator.HeavyDbIndexOperation.DropInternalTransactionsCreatedContractAddressHashPartialIndex do
@moduledoc """
Drops index "internal_transactions_created_contract_address_hash_partial_index" on
internal_transactions(created_contract_address_hash, block_number DESC, transaction_index DESC, index DESC).
"""
use Explorer.Migrator.HeavyDbIndexOperation
alias Explorer.Chain.Cache.BackgroundMigrations
alias Explorer.Migrator.{EmptyInternalTransactionsData, HeavyDbIndexOperation, MigrationStatus}
alias Explorer.Migrator.HeavyDbIndexOperation.Helper, as: HeavyDbIndexOperationHelper
@table_name :internal_transactions
@index_name "internal_transactions_created_contract_address_hash_partial_index"
@operation_type :drop
@impl HeavyDbIndexOperation
def table_name, do: @table_name
@impl HeavyDbIndexOperation
def operation_type, do: @operation_type
@impl HeavyDbIndexOperation
def index_name, do: @index_name
@impl HeavyDbIndexOperation
def dependent_from_migrations do
[EmptyInternalTransactionsData.migration_name()]
end
@impl HeavyDbIndexOperation
def db_index_operation do
HeavyDbIndexOperationHelper.safely_drop_db_index(@index_name)
end
@impl HeavyDbIndexOperation
def check_db_index_operation_progress do
operation = HeavyDbIndexOperationHelper.drop_index_query_string(@index_name)
HeavyDbIndexOperationHelper.check_db_index_operation_progress(@index_name, operation)
end
@impl HeavyDbIndexOperation
def db_index_operation_status do
HeavyDbIndexOperationHelper.db_index_dropping_status(@index_name)
end
@impl HeavyDbIndexOperation
def restart_db_index_operation do
HeavyDbIndexOperationHelper.safely_drop_db_index(@index_name)
end
@impl HeavyDbIndexOperation
def running_other_heavy_migration_exists?(migration_name) do
MigrationStatus.running_other_heavy_migration_for_table_exists?(@table_name, migration_name)
end
@impl HeavyDbIndexOperation
def update_cache do
BackgroundMigrations.set_heavy_indexes_drop_internal_transactions_created_contract_address_hash_partial_index_finished(
true
)
end
end
Valid values for @table_name (from behavior typespec):
:addresses:address_coin_balances:address_current_token_balances:address_token_balances:blocks:internal_transactions:logs:token_transfers:tokens:transactionsAll generated modules must be placed in:
apps/explorer/lib/explorer/migrator/heavy_db_index_operation/
Standard aliases to include:
alias Explorer.Chain.Cache.BackgroundMigrations
alias Explorer.Migrator.{HeavyDbIndexOperation, MigrationStatus}
alias Explorer.Migrator.HeavyDbIndexOperation.Helper, as: HeavyDbIndexOperationHelper
For dependencies, add specific aliases:
alias Explorer.Migrator.EmptyInternalTransactionsData
HeavyDbIndexOperationHelper.create_db_index/1 - With query stringHeavyDbIndexOperationHelper.create_db_index/3 - With index name, table, columnsHeavyDbIndexOperationHelper.create_index_query_string/3 - Generate query stringHeavyDbIndexOperationHelper.db_index_creation_status/1 - Check creation statusHeavyDbIndexOperationHelper.add_concurrently_flag?/0 - For CONCURRENT keywordHeavyDbIndexOperationHelper.safely_drop_db_index/1 - Drop with safety checksHeavyDbIndexOperationHelper.drop_index_query_string/1 - Generate drop queryHeavyDbIndexOperationHelper.db_index_dropping_status/1 - Check dropping statusHeavyDbIndexOperationHelper.check_db_index_operation_progress/2 - Monitor progressWhen dropping indexes, you may need to invalidate caches as shown in the module docstring:
@impl HeavyDbIndexOperation
def restart_db_index_operation do
HeavyDbIndexOperationHelper.safely_drop_db_index(@index_name)
BackgroundMigrations.invalidate_cache(__MODULE__.migration_name())
end
For rename operations (typically used after create + drop to swap indexes):
defmodule Explorer.Migrator.HeavyDbIndexOperation.RenameTransactions2ndCreatedContractAddressHashWithPendingIndexA do
@moduledoc """
Renames index "transactions_2nd_created_contract_address_hash_with_pending_index_a"
to "transactions_created_contract_address_hash_with_pending_index_a".
"""
use Explorer.Migrator.HeavyDbIndexOperation
require Logger
alias Explorer.Chain.Cache.BackgroundMigrations
alias Explorer.Migrator.{HeavyDbIndexOperation, MigrationStatus}
alias Explorer.Migrator.HeavyDbIndexOperation.Helper, as: HeavyDbIndexOperationHelper
alias Explorer.Migrator.HeavyDbIndexOperation.DropTransactionsCreatedContractAddressHashWithPendingIndexA
alias Explorer.Repo
@table_name :transactions
@old_index_name "transactions_2nd_created_contract_address_hash_with_pending_index_a"
@new_index_name "transactions_created_contract_address_hash_with_pending_index_a"
@operation_type :rename
# Note: migration_name will be:
# "heavy_indexes_rename_transactions_created_contract_address_hash_with_pending_index_a"
@impl HeavyDbIndexOperation
def table_name, do: @table_name
@impl HeavyDbIndexOperation
def operation_type, do: @operation_type
@impl HeavyDbIndexOperation
def index_name, do: @new_index_name
@impl HeavyDbIndexOperation
def dependent_from_migrations do
[DropTransactionsCreatedContractAddressHashWithPendingIndexA.migration_name()]
end
@impl HeavyDbIndexOperation
# sobelow_skip ["SQL"]
def db_index_operation do
case Repo.query(rename_index_query_string(), [], timeout: :infinity) do
{:ok, _} ->
:ok
{:error, error} ->
Logger.error("Failed to rename index from #{@old_index_name} to #{@new_index_name}: #{inspect(error)}")
:error
end
end
@impl HeavyDbIndexOperation
def check_db_index_operation_progress do
HeavyDbIndexOperationHelper.check_db_index_operation_progress(@new_index_name, rename_index_query_string())
end
@impl HeavyDbIndexOperation
def db_index_operation_status do
old_index_status = HeavyDbIndexOperationHelper.db_index_exists_and_valid?(@old_index_name)
new_index_status = HeavyDbIndexOperationHelper.db_index_exists_and_valid?(@new_index_name)
cond do
# Rename completed: old index doesn't exist, new index exists and is valid
old_index_status == %{exists?: false, valid?: nil} and new_index_status == %{exists?: true, valid?: true} ->
:completed
# Rename not started: old index exists, new index doesn't exist
old_index_status == %{exists?: true, valid?: true} and new_index_status == %{exists?: false, valid?: nil} ->
:not_initialized
# Unknown state
true ->
:unknown
end
end
@impl HeavyDbIndexOperation
def restart_db_index_operation do
# To restart, we need to rename back to the old name
case Repo.query(reverse_rename_index_query_string(), [], timeout: :infinity) do
{:ok, _} ->
:ok
{:error, error} ->
Logger.error("Failed to reverse rename index from #{@new_index_name} to #{@old_index_name}: #{inspect(error)}")
:error
end
end
@impl HeavyDbIndexOperation
def running_other_heavy_migration_exists?(migration_name) do
MigrationStatus.running_other_heavy_migration_for_table_exists?(@table_name, migration_name)
end
@impl HeavyDbIndexOperation
def update_cache do
BackgroundMigrations.set_heavy_indexes_rename_transactions_created_contract_address_hash_with_pending_index_a_finished(
true
)
end
defp rename_index_query_string do
"ALTER INDEX #{@old_index_name} RENAME TO #{@new_index_name};"
end
defp reverse_rename_index_query_string do
"ALTER INDEX #{@new_index_name} RENAME TO #{@old_index_name};"
end
end
When to use rename operations:
Important notes for rename operations:
@operation_type :rename (not :create)index_name/0 should return the new (final) index nameheavy_indexes_rename_{new_index_name_lowercase}index_name = "transactions_created_contract_address_hash_with_pending_index_a", the migration name is "heavy_indexes_rename_transactions_created_contract_address_hash_with_pending_index_a"After creating migration modules, you must update the cache tracking in
apps/explorer/lib/explorer/chain/cache/background_migrations.ex:
Add keys for each new migration at the top of the module:
use Explorer.Chain.MapCache,
name: :background_migrations_status,
# ... existing keys ...
key: :heavy_indexes_create_transactions_2nd_created_contract_address_hash_with_pending_index_a_finished,
key: :heavy_indexes_drop_transactions_created_contract_address_hash_with_pending_index_a_finished,
key: :heavy_indexes_rename_transactions_created_contract_address_hash_with_pending_index_a_finished
Add aliases in the HeavyDbIndexOperation alias block:
alias Explorer.Migrator.HeavyDbIndexOperation.{
# ... existing aliases ...
CreateTransactions2ndCreatedContractAddressHashWithPendingIndexA,
DropTransactionsCreatedContractAddressHashWithPendingIndexA,
RenameTransactions2ndCreatedContractAddressHashWithPendingIndexA
}
Add handle_fallback/1 functions for each migration:
defp handle_fallback(:heavy_indexes_create_transactions_2nd_created_contract_address_hash_with_pending_index_a_finished) do
set_and_return_migration_status(
CreateTransactions2ndCreatedContractAddressHashWithPendingIndexA,
&set_heavy_indexes_create_transactions_2nd_created_contract_address_hash_with_pending_index_a_finished/1
)
end
defp handle_fallback(:heavy_indexes_drop_transactions_created_contract_address_hash_with_pending_index_a_finished) do
set_and_return_migration_status(
DropTransactionsCreatedContractAddressHashWithPendingIndexA,
&set_heavy_indexes_drop_transactions_created_contract_address_hash_with_pending_index_a_finished/1
)
end
defp handle_fallback(:heavy_indexes_rename_transactions_created_contract_address_hash_with_pending_index_a_finished) do
set_and_return_migration_status(
RenameTransactions2ndCreatedContractAddressHashWithPendingIndexA,
&set_heavy_indexes_rename_transactions_created_contract_address_hash_with_pending_index_a_finished/1
)
end
Cache key naming convention:
heavy_indexes_{operation}_{snake_case_index_name}_finishedcreate, drop, rename, etc._finishedAdd each migration module to the application supervisor in
apps/explorer/lib/explorer/application.ex:
Find the section with other heavy DB index operations and add:
configure_mode_dependent_process(
Explorer.Migrator.HeavyDbIndexOperation.CreateTransactions2ndCreatedContractAddressHashWithPendingIndexA,
:indexer
),
configure_mode_dependent_process(
Explorer.Migrator.HeavyDbIndexOperation.DropTransactionsCreatedContractAddressHashWithPendingIndexA,
:indexer
),
configure_mode_dependent_process(
Explorer.Migrator.HeavyDbIndexOperation.RenameTransactions2ndCreatedContractAddressHashWithPendingIndexA,
:indexer
),
Important: These entries must be added to start the migration processes during application startup.
Each migration module must implement update_cache/0:
@impl HeavyDbIndexOperation
def update_cache do
BackgroundMigrations.set_heavy_indexes_create_my_index_finished(true)
end
The setter function name follows: set_heavy_indexes_{operation}_{index_name}_finished/1
Create*/Drop*/Rename* convention@moduledoc describes the index and its columnsuse Explorer.Migrator.HeavyDbIndexOperation declared near module top@table_name, @index_name, @operation_type module attributes defined@table_columns OR @query_string (or custom for rename)dependent_from_migrations/0apps/explorer/lib/explorer/migrator/heavy_db_index_operation/update_cache/0 implemented with correct setter nameconfigure_mode_dependent_process entry❌ Incorrect table name - Must be one of the supported atoms
❌ Missing dependencies - If index depends on other migrations, specify them
❌ Wrong helper function - Use creation helpers for :create, dropping helpers for :drop
❌ Inconsistent naming - Index name should match module name semantically
❌ Missing CONCURRENT - Use add_concurrently_flag?() in query strings
❌ No progress tracking - Always implement check_db_index_operation_progress/0
❌ Forgot cache updates - Must update BackgroundMigrations cache module
❌ Missing update_cache/0 - Every module must implement this callback
When replacing an existing index with a new version (e.g., adding a WHERE clause):
_2nd_ prefix)
This ensures zero downtime - the old index remains available until the new one is ready.