.ai/principles/distilled/database-schema.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.
add_concurrent_partitioned_index insteadadd_concurrent_index for indexes on populated tables (never inside a transactional migration)WHERE clause) when queries always filter on a known condition and target a subset of datatmp_ and create a follow-up issue to remove them in the next milestoneANALYZE explicitly in the post-deployment migration (only for non-large tables)prepare_async_index / prepare_partitioned_async_index for very large tables to schedule index creation during low-traffic windowsname: argument for any index created with where, using, order, length, type, or opclass optionsindex_<table>_on_<column>[_and_<column>]*; keep names all lowercaseindex_name_exists? (or index_exists? with name:) to test for index existence by namenulls_not_distinct: true on unique indexes when you need to enforce full uniqueness including NULL values, instead of combining two separate indexesprepare_async_index_removal to schedule index removal asynchronously, then add a synchronous follow-up migration after verifying removal in productionbigint, even if the referenced table has an integer primary keyON DELETE clause on every foreign key (use CASCADE in 99% of cases)add_concurrent_foreign_key with validate: false when adding a FK to an existing column, then validate in a separate migrationadd_foreign_key or add_concurrent_foreign_key more than once per migration file unless source and target tables are identicalreverse_lock_order: true for high-traffic tables when adding or removing foreign keys to avoid deadlocksremove_partitioned_foreign_key instead of remove_foreign_key when removing FKs from partitioned tablesdependent: :destroy or dependent: :delete on associations; let the database handle cascading deletes via FK constraintsbefore_destroy or after_destroy callbacks unless approved by database specialists; use service classes for non-database cleanup_id suffix only for columns referencing another table; use _xid for third-party platform IDs_id suffix to ignored_fk_columns_map in spec/db/schema_spec.rb only when they meet the documented criteria (cross-schema, loose FK, polymorphic, or non-reference)prepare_async_foreign_key_validation / prepare_partitioned_async_foreign_key_validation to schedule validation during low-traffic windowsNOT NULL constraints to all columns that should never be NULLNOT NULL directly in create_tableNOT VALID constraint → fix existing records → validate in next releaseadd_not_null_constraint helper (not raw ALTER TABLE) for adding constraints to existing columnsbelongs_to associations requiring presence, use optional: false instead of a separate validates :field, presence: true; note that config.active_record.belongs_to_required_by_default = false is set in GitLab, so mark required associations explicitlyNOT NULL constraint in a subsequent releasevalidate: false first, then use prepare_async_check_constraint_validation for asynchronous validationNOT NULL constraint from an individual partition; drop it from the parent table so it cascadesadd_multi_column_not_null_constraint when enforcing that a specific number of columns across a set must be non-null (e.g., exactly one of project_id or group_id must be present)text data type instead of string for all string/text columnstext columns: use limit: in create_table or add_text_limit on existing tablesNOT VALID in a post-deployment migration, fix existing records, then validate in the next releasetext columns for encrypts attributes; use :jsonb columns insteadSMALLINT (limit: 2) for all new enum columnssource_type/source_id pattern); use separate tables for each type instead*_type columns as a pattern for future polymorphic expansionself.inheritance_column = :_type_disabled) when using models in migrationsdefine_batchable_model helper in migrations instead of defining a model class when only STI disabling or EachBatch is neededEnumInheritance concern instead of storing the class name stringCHECK constraints to enforce data integrity rules beyond NOT NULLCHECK constraint with a default value that satisfies the constraint, add with validate: false in the regular migration and validate in a post-deployment migrationcheck_<table>_<column>[_<suffix>]pk_<table>, fk_<table>_<col>_<foreign_table>, index_<table>_on_<col>, unique_<table>_<col>, check_<table>_<col>[_<suffix>], excl_<table>_<col>[_<suffix>]_and_ joiners if neededdb/structure.sql for naming conflicts before adding new constraintsFor the full picture, see: