Write Rails database migrations following project conventions: UUID primary keys with pgcrypto, named indexes, array columns, bulk mode, and proper column defaults. Use this skill whenever creating or modifying database tables, adding columns, creating indexes, or writing any ActiveRecord migration. Triggers on: 'create migration', 'add column', 'create table', 'add index', 'change column', 'database change', 'new migration', 'schema change', or any task that requires a db/migrate file.
This skill ensures every migration follows the project's established database conventions. The project uses PostgreSQL with UUID-based primary keys, and every table follows a consistent structure.
Every table uses uuid as its primary identifier instead of auto-incrementing integers. This is a deliberate architectural choice for distributed-safe IDs and cross-service references.
Enable the pgcrypto extension at the top of any migration that creates a new table (idempotently):
enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto')
Then define the uid column as a non-null UUID with a database-level default:
t.uuid :uid, null: false, default: 'gen_random_uuid()'
Always add a unique index on :
uidt.index :uid, name: 'index_<table_name>_on_uid', unique: true
Use bulk: true on create_table to batch all column additions into a single ALTER TABLE statement. This is faster for PostgreSQL:
create_table :widgets, bulk: true do |t|
# columns here
end
Always name indexes explicitly rather than relying on Rails auto-naming. The pattern is:
index_<table_name>_on_<column_name>
This prevents issues with index name length limits and makes migrations reversible with clarity.
When referencing another table, use t.uuid with the naming convention <associated_table_singular>_uid:
t.uuid :organisation_uid # references organisations.uid
t.uuid :parent_org_uid # references organisations.uid (self-referential)
Do NOT use t.references with UUID tables -- use explicit t.uuid columns and add indexes manually.
PostgreSQL array columns are used for denormalized lists. Always provide a default of []:
t.string :sub_faculties, array: true, default: []
t.string :affiliation_types, array: true, default: []
Enum columns use t.integer with a default of 0 (the first enum value):
t.integer :status, default: 0
t.integer :org_type
Boolean columns always have an explicit default:
t.boolean :allow_view_only, default: false
t.boolean :is_active, default: true
Every table includes timestamps:
t.timestamps
# frozen_string_literal: true
class CreateWidgets < ActiveRecord::Migration[7.0]
def change
enable_extension 'pgcrypto' unless extension_enabled?('pgcrypto')
create_table :widgets, bulk: true do |t|
t.uuid :uid, null: false, default: 'gen_random_uuid()'
t.uuid :organisation_uid
t.string :name
t.string :code
t.text :description
t.integer :status, default: 0
t.integer :widget_type
t.boolean :is_active, default: true
t.string :tags, array: true, default: []
t.timestamps
t.index :uid, name: 'index_widgets_on_uid', unique: true
t.index :code, name: 'index_widgets_on_code', unique: true
t.index :organisation_uid, name: 'index_widgets_on_organisation_uid'
end
end
end
# frozen_string_literal: true
class AddDetailsToWidgets < ActiveRecord::Migration[7.0]
def change
add_column :widgets, :priority, :integer, default: 0
add_column :widgets, :metadata, :jsonb, default: {}
add_index :widgets, :priority, name: 'index_widgets_on_priority'
end
end
# frozen_string_literal: true
class AddUniqueIndexToWidgets < ActiveRecord::Migration[7.0]
def change
add_index :widgets, %i[name organisation_uid],
name: 'index_widgets_on_name_and_org_uid',
unique: true
end
end
Rails generates the timestamp prefix automatically. The file name convention:
create_<table_name>.rbadd_<column>_to_<table>.rbremove_<column>_from_<table>.rbadd_unique_index_to_<table>.rbrename_<old>_to_<new>_in_<table>.rbuid column? (Yes, always for new tables)t.uuid with _uid suffix?t.integer?bulk: true set on create_table?[]?pgcrypto extension enabled?