For CLI commands (
kit migrate, kit migrate:rollback, etc.), see the CLI Migrations Reference.Creating Migrations
Generate a new migration file:kit make:migration create_users_table
migrations/ directory:
migrations/
├── mod.rs
└── m20240115_120000_create_users_table.rs
Migration Structure
Every migration has two methods:use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
// Run the migration
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// Create tables, add columns, etc.
}
// Reverse the migration
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
// Drop tables, remove columns, etc.
}
}
Schema Operations
Creating Tables
use sea_orm_migration::prelude::*;
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Users::Table)
.if_not_exists()
.col(
ColumnDef::new(Users::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(Users::Email).string().not_null().unique_key())
.col(ColumnDef::new(Users::Name).string().not_null())
.col(ColumnDef::new(Users::PasswordHash).string().not_null())
.col(ColumnDef::new(Users::CreatedAt).timestamp().not_null())
.col(ColumnDef::new(Users::UpdatedAt).timestamp().not_null())
.to_owned(),
)
.await
}
// Define the table and column identifiers
#[derive(DeriveIden)]
enum Users {
Table,
Id,
Email,
Name,
PasswordHash,
CreatedAt,
UpdatedAt,
}
Dropping Tables
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Users::Table).to_owned())
.await
}
Column Types
| Method | Database Type | Notes |
|---|---|---|
integer() | INTEGER | 32-bit integer |
big_integer() | BIGINT | 64-bit integer |
small_integer() | SMALLINT | 16-bit integer |
float() | FLOAT | Floating point |
double() | DOUBLE | Double precision |
decimal() | DECIMAL | Fixed-point |
string() | VARCHAR(255) | Variable length string |
string_len(n) | VARCHAR(n) | Custom length string |
text() | TEXT | Long text |
boolean() | BOOLEAN | True/false |
timestamp() | TIMESTAMP | Date and time |
date() | DATE | Date only |
time() | TIME | Time only |
blob() | BLOB | Binary data |
json() | JSON | JSON data |
uuid() | UUID | UUID type |
Column Modifiers
ColumnDef::new(Column::Name)
.string()
.not_null() // NOT NULL constraint
.null() // Allows NULL (default)
.default("value") // Default value
.unique_key() // UNIQUE constraint
.primary_key() // PRIMARY KEY
.auto_increment() // AUTO_INCREMENT
Adding Columns
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Users::Table)
.add_column(
ColumnDef::new(Users::PhoneNumber)
.string()
.null()
)
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Users::Table)
.drop_column(Users::PhoneNumber)
.to_owned(),
)
.await
}
Modifying Columns
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Users::Table)
.modify_column(
ColumnDef::new(Users::Name)
.string_len(500) // Change VARCHAR(255) to VARCHAR(500)
.not_null()
)
.to_owned(),
)
.await
}
Renaming Columns
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.alter_table(
Table::alter()
.table(Users::Table)
.rename_column(Users::Name, Users::FullName)
.to_owned(),
)
.await
}
Indexes
Creating Indexes
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_index(
Index::create()
.name("idx_users_email")
.table(Users::Table)
.col(Users::Email)
.unique() // Optional: make it unique
.to_owned(),
)
.await
}
Composite Indexes
manager
.create_index(
Index::create()
.name("idx_posts_user_created")
.table(Posts::Table)
.col(Posts::UserId)
.col(Posts::CreatedAt)
.to_owned(),
)
.await
Dropping Indexes
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_index(Index::drop().name("idx_users_email").to_owned())
.await
}
Foreign Keys
Adding Foreign Keys
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Posts::Table)
.if_not_exists()
.col(
ColumnDef::new(Posts::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(Posts::UserId).integer().not_null())
.col(ColumnDef::new(Posts::Title).string().not_null())
.col(ColumnDef::new(Posts::Content).text().not_null())
.foreign_key(
ForeignKey::create()
.name("fk_posts_user")
.from(Posts::Table, Posts::UserId)
.to(Users::Table, Users::Id)
.on_delete(ForeignKeyAction::Cascade)
.on_update(ForeignKeyAction::Cascade),
)
.to_owned(),
)
.await
}
Foreign Key Actions
| Action | Description |
|---|---|
Cascade | Delete/update child rows automatically |
SetNull | Set foreign key to NULL |
SetDefault | Set foreign key to default value |
Restrict | Prevent delete/update if referenced |
NoAction | Similar to Restrict |
Migration Workflow
1. Create Migration
kit make:migration create_posts_table
2. Edit the Migration
// migrations/m20240115_130000_create_posts_table.rs
use sea_orm_migration::prelude::*;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.create_table(
Table::create()
.table(Posts::Table)
.if_not_exists()
.col(
ColumnDef::new(Posts::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(Posts::Title).string().not_null())
.col(ColumnDef::new(Posts::Body).text().not_null())
.col(ColumnDef::new(Posts::Published).boolean().not_null().default(false))
.col(ColumnDef::new(Posts::CreatedAt).timestamp().not_null())
.col(ColumnDef::new(Posts::UpdatedAt).timestamp().not_null())
.to_owned(),
)
.await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager
.drop_table(Table::drop().table(Posts::Table).to_owned())
.await
}
}
#[derive(DeriveIden)]
enum Posts {
Table,
Id,
Title,
Body,
Published,
CreatedAt,
UpdatedAt,
}
3. Run the Migration
kit migrate
4. Sync Entities
kit db:sync
src/models/.
Best Practices
Always Write Down Migrations
Always implementdown() to allow rollbacks:
// Good: Reversible migration
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.create_table(/* ... */).await
}
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
manager.drop_table(/* ... */).await
}
Use Descriptive Names
# Good: Describes the change
kit make:migration add_email_verified_to_users
kit make:migration create_order_items_table
kit make:migration add_index_to_posts_slug
# Bad: Vague names
kit make:migration update_users
kit make:migration change_table
One Change Per Migration
Keep migrations focused on a single change:# Good: Separate migrations
kit make:migration create_categories_table
kit make:migration add_category_id_to_posts
# Avoid: Multiple unrelated changes in one migration
Test Migrations Both Ways
Before committing, verify both directions work:kit migrate # Apply
kit migrate:rollback # Rollback
kit migrate # Apply again
CLI Commands Reference
| Command | Description |
|---|---|
kit make:migration <name> | Create a new migration |
kit migrate | Run all pending migrations |
kit migrate:status | Show migration status |
kit migrate:rollback | Rollback last migration |
kit migrate:rollback --step 3 | Rollback last 3 migrations |
kit migrate:fresh | Drop all tables and re-run |
kit db:sync | Sync schema to entities |