.ai/principles/distilled/database-migrations.md
Prerequisite: If you haven't already, also read .ai/principles/distilled/database-fundamentals.md - it contains foundational rules that apply to all database work.
db/migrate) for schema changes critical to application speed or behavior that complete in ≤ 3 minutesdb/post_migrate) for non-critical schema changes (column removals, non-critical indexes) and data migrations completing in ≤ 10 minutescreate_table or add_column operations — these must be regular schema migrationsmilestone on every new migration (required since GitLab 16.6)Gitlab::Database::Migration[<latest_version>] (e.g., [2.1]) — DO NOT include Gitlab::Database::MigrationHelpers directlyGitlab::Database::Migration (look up Gitlab::Database::Migration::MIGRATION_CLASSES)MigrationRecord and set self.table_name explicitlyreset_column_information on any model before using it after a schema change in the same migration rundown methoddown method with a # no-op comment explaining whydisable_ddl_transaction! when using add_concurrent_index, add_concurrent_foreign_key, or any operation that must run outside a single transactionwith_lock_retries for DDL on high-traffic tables to avoid lock contentionwith_lock_retries inside the change method — define explicit up/down methodseach_batch_range or batched background migrationsadd_concurrent_index (with disable_ddl_transaction!) for adding indexes on non-empty tablesremove_concurrent_index (with disable_ddl_transaction!) for removing indexes on non-empty tablesremove_index in a single-transaction migration only for tables with fewer than 1,000 recordsdb:gitlabcom-database-testing reports index creation exceeding 20 minutes, create the index asynchronouslyi_ instead of index_, skip redundant prefixes, or use a purpose-based nameadd_concurrent_foreign_key for adding foreign keys (has lock retries built in)with_lock_retries when removing foreign keys on high-traffic tablesvalidate: false) and validation (prepare_async_foreign_key_validation) across different migrationsignore_column with remove_with and remove_after attributes when ignoring a columnignore_column to the CE modelignore_columns to the corresponding view-backed model classrename_column_concurrently + cleanup_concurrent_column_rename (across two migrations) for zero-downtime column renameschange_column_type_concurrently + cleanup_concurrent_column_type_change for zero-downtime column type changesNOT NULL constraints in post-deployment migrations (after application code is deployed); remove NOT NULL constraints in regular migrationschange_column to add/remove constraints — it rewrites the entire column definition inefficientlySafelyChangeColumnDefault two-release process when changing a column default that application code may explicitly writedb/docs/deleted_tables per the database dictionary guideTABLES_TO_BE_RENAMED in lib/gitlab/database.rb one release before executing rename_table_safelyrename_table_safely / undo_rename_table_safely in a standard (non-post) migrationfinalize_table_rename in a post-deployment migration of the same release as the renameTABLES_TO_BE_RENAMED in the same release as finalize_table_renamedb/docs and create an entry for the interim view in db/docs/deleted_viewsself.primary_key in the model before deploying the rename migrationbigint (:integer, limit: 8) for columns that may exceed 2 GB or for IDs on large tablesadd_timestamps_with_timezone, timestamps_with_timezone, or datetime_with_timezone instead of add_timestamps, timestamps, or :datetimeencrypts attributes as :jsonb, not :textJsonSchemaValidator with a size_limit (recommended max 64 KB) for all JSONB columnsadditionalProperties: false: add the property to the schema first (without marking it required), then add code that uses it after full deployment; for removal, remove code first, then data, then the schema entry — each step in a separate release for self-managed, or after full deployment for GitLab.com-only propertiesquote_stringupdate_column_in_batches or each_batch_rangeArel.sql to wrap computed SQL values passed to update_column_in_batchesscripts/refresh-migrations-timestamps when rebasing old branchesdb/structure.sql changes generated by bundle exec rails db:migrate — DO NOT edit it manuallydb/structure.sql for existing tablesdb/schema_migrations/<timestamp> checksum file in the MR that adds the migrationdb/docs/deleted_tableswith_lock_retries for any DDL on high-traffic tableswith_lock_retries with idempotent replace: true / if_exists: true guardsbundle exec rails g batched_background_migration) to scaffold BBM files so all required files are created by defaultqueue_batched_background_migration; DO NOT enqueue them in regular migrationsGitlab::BackgroundMigration namespace in lib/gitlab/background_migration/cursor DSL) as the default strategy for new BBMs; omit cursor only when the legacy primary-key strategy is explicitly requiredjob_arguments helper to declare job arguments; queue_batched_background_migration raises an error if the count does not matchapp/models) in BBMs; define inline models inheriting from the correct ApplicationRecord subclass (::ApplicationRecord for main, ::Ci::ApplicationRecord for ci); DO NOT use ActiveRecord::BaseActiveRecord::Base.connection in BBMs; use the model's connection or ApplicationRecord.connection insteadLooseIndexScanBatchingStrategy and distinct_each_batch instead of the default primary-key strategyscope_to only when the scoped condition is covered by an index with an index-only scan; disable the Database/AvoidScopeTo cop with a comment citing the supporting indextables_to_check_for_vacuum when the BBM iterates over one table but writes to different tables, to avoid unnecessary autovacuum pauses on the iteration tableensure_batched_background_migration_is_finished in a post-deployment migrationgitlab_schema exactly in ensure_batched_background_migration_is_finished — even if the schema label changed since enqueueingfinalized_by in the corresponding db/docs/batched_background_migrations/<name>.yml when adding the finalization migrationup/down and call delete_batched_background_migration at the start of the new migration's updelete_batched_background_migrationdb/schema_migrations/ are auto-generated and do not require a newline at the end -- do not flag missing newlinesWhen creating a db/docs/batched_background_migrations/<name>.yml, the YAML MUST include:
migration_job_name: <BBM class name in CamelCase>description: <one-line description>feature_category: <category symbol>introduced_by_url: <MR URL> (placeholder OK for unreleased)milestone: '<X.Y>'queued_migration_version: <version timestamp>gitlab_schema: <gitlab_main | gitlab_ci | gitlab_main_user | gitlab_main_org> — match the schema of the BBM's primary tablefinalized_by: <version>For the full picture, see: