ORM Party

Migrations

Select ORMs to compare

Pick one or more ORMs from the bar above

Generate Migration

SQL

-- No migration tool; write SQL manually

ALTER TABLE users
  ADD COLUMN age INTEGER;

Django ORM

# Add a new field to the model

class User(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField(unique=True)
    age = models.IntegerField(null=True)  # ← new

SQLAlchemy

# Add a new column to the model

class User(Base):
    __tablename__ = "users"
    id = mapped_column(Integer, primary_key=True)
    name = mapped_column(String(100))
    email = mapped_column(String, unique=True)
    age = mapped_column(Integer, nullable=True)  # ← new

SQLModel

# Add a new field to the model

class User(SQLModel, table=True):
    __tablename__ = "users"

    id: int | None = Field(default=None, primary_key=True)
    name: str = Field(max_length=100)
    email: str = Field(unique=True, max_length=255)
    age: int | None = Field(default=None)  # ← new

Tortoise ORM

# Add a new field to the model

class User(Model):
    name = fields.CharField(max_length=100)
    email = fields.CharField(max_length=255, unique=True)
    age = fields.IntField(null=True)  # ← new

Peewee

# Add a new field to the model

class User(Model):
    name = CharField(max_length=100)
    email = CharField(max_length=255, unique=True)
    age = IntegerField(null=True)  # ← new

Prisma

// Add a new field to the schema

model User {
  id    Int     @id @default(autoincrement())
  name  String
  email String  @unique
  age   Int?    // ← new
}

Drizzle

// Add a new column to the schema

export const users = pgTable("users", {
  id: serial("id").primaryKey(),
  name: text("name").notNull(),
  email: text("email").unique(),
  age: integer("age"),  // ← new
});

Kysely

// Add a new column type to DB interface
export interface UserTable {
  id: number
  name: string
  email: string
  age: number | null // ← new
}

TypeORM

// Add a new column to the entity

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column({ unique: true })
  email: string;

  @Column({ nullable: true })
  age: number;  // ← new
}

MikroORM

// Add a new field to the entity

@Entity({ tableName: "users" })
export class User {
  @PrimaryKey()
  id!: number;

  @Property({ length: 100 })
  name!: string;

  @Property({ length: 255, unique: true })
  email!: string;

  @Property({ nullable: true })
  age?: number; // ← new
}

Sequelize

// Add a new field to the model

class User extends Model {
  declare id: CreationOptional<number>;
  declare name: string;
  declare email: string;
  declare age: number | null; // ← new
}

User.init(
  {
    id: { type: DataTypes.INTEGER, autoIncrement: true, primaryKey: true },
    name: { type: DataTypes.STRING(100), allowNull: false },
    email: { type: DataTypes.STRING(255), allowNull: false, unique: true },
    age: { type: DataTypes.INTEGER, allowNull: true },
  },
  { sequelize, tableName: "users" },
);

Apply Migration

SQL

-- Run migration file manually
\i migrations/0002_add_user_age.sql

-- Or via command line
-- psql -d mydb -f migrations/0002_add_user_age.sql

Django ORM

$ python manage.py migrate

# Operations to perform:
#   Apply all: myapp
# Running migrations:
#   Applying myapp.0002_user_age... OK

# Apply only a specific app
$ python manage.py migrate myapp

SQLAlchemy

$ alembic upgrade head

# INFO [alembic.runtime.migration]
#   Running upgrade  -> ae10, add user age

# Apply next migration only
$ alembic upgrade +1

SQLModel

$ alembic upgrade head

# INFO [alembic.runtime.migration]
#   Running upgrade  -> ae10, add user age

# Apply next migration only
$ alembic upgrade +1

Tortoise ORM

$ tortoise migrate

# Applying models.0002_add_user_age... OK

# Apply only a specific app
$ tortoise migrate models

Peewee

# Run migration scripts manually
$ python migrations/20240115_add_user_age.py

# Executes upgrade() and adds users.age

Prisma

# Development (generates + applies)
$ npx prisma migrate dev

# Applying migration `20240101_add_user_age`
# ✔ Generated Prisma Client

# Production (applies only)
$ npx prisma migrate deploy

Drizzle

$ npx drizzle-kit migrate

# [✓] migrations applied successfully
#   0001_add_user_age.sql

Kysely

# Apply migrations with Kysely Migrator
$ bun run migrate:up

# Applying migration:
#   20240115_add_user_age

TypeORM

$ npx typeorm migration:run -d data-source.ts

# query: SELECT * FROM "migrations"
# 1 migration is pending
# query: ALTER TABLE "user" ADD "age" integer
# Migration AddUserAge1234567890 has been
#   executed successfully

MikroORM

$ npx mikro-orm migration:up

# [migrator] Applying Migration20240115000000

# Apply up to specific migration
$ npx mikro-orm migration:up --to Migration20240115000000

Sequelize

$ npx sequelize-cli db:migrate

# == 20240115000000-add-user-age: migrated

# Apply migrations up to a specific file
$ npx sequelize-cli db:migrate --to 20240115000000-add-user-age.js

Rollback Migration

SQL

-- Write reverse migration manually

-- rollback: 0002_add_user_age.sql
ALTER TABLE users DROP COLUMN age;

Django ORM

# Revert to a previous migration
$ python manage.py migrate myapp 0001_initial

# Operations to perform:
#   Target: 0001_initial
# Running migrations:
#   Unapplying myapp.0002_user_age... OK
# Django auto-generates the reverse operation
# (drops the "age" column)

# Revert all migrations for an app
$ python manage.py migrate myapp zero

SQLAlchemy

# Rollback last migration
$ alembic downgrade -1

# INFO [alembic.runtime.migration]
#   Running downgrade ae10 -> 5ba1, add user age
# Executes the downgrade() function:
#   op.drop_column("users", "age")

# Rollback to a specific revision
$ alembic downgrade ae1027a6acf

SQLModel

# Rollback last migration
$ alembic downgrade -1

# INFO [alembic.runtime.migration]
#   Running downgrade ae10 -> 5ba1, add user age
# Executes the downgrade() function:
#   op.drop_column("users", "age")

# Rollback to a specific revision
$ alembic downgrade ae1027a6acf

Tortoise ORM

# Revert to a previous migration
$ tortoise downgrade models 0001_initial

# Unapplying models.0002_add_user_age... OK

Peewee

# Rollback by running reverse migration
$ python migrations/20240115_add_user_age.py --downgrade

# Executes downgrade() and drops users.age

Prisma

# Prisma has no built-in rollback command.
# Options:

# 1. Revert schema and create a new migration
#    (remove the "age" field from schema.prisma)
$ npx prisma migrate dev --name revert_user_age

# 2. Manually run reverse SQL
$ npx prisma db execute --file rollback.sql
# rollback.sql:
#   ALTER TABLE "User" DROP COLUMN "age";

Drizzle

# Drizzle has no built-in rollback.
# Options:

# 1. Revert schema and generate a new migration
#    (remove the "age" column from schema.ts)
$ npx drizzle-kit generate

# 2. Use drizzle-kit push for dev databases
$ npx drizzle-kit push

Kysely

# Rollback last migration
$ bun run migrate:down

# Reverted migration:
#   20240115_add_user_age

TypeORM

# Revert last applied migration
$ npx typeorm migration:revert -d data-source.ts

# query: SELECT * FROM "migrations"
# Reverting AddUserAge1234567890
# query: ALTER TABLE "user" DROP COLUMN "age"
# Migration AddUserAge1234567890 has been
#   reverted successfully
# Executes the down() method of the migration

MikroORM

# Revert last applied migration
$ npx mikro-orm migration:down

# [migrator] Reverting Migration20240115000000

# Revert down to a specific migration
$ npx mikro-orm migration:down --to Migration20240101000000

Sequelize

# Revert last applied migration
$ npx sequelize-cli db:migrate:undo

# == 20240115000000-add-user-age: reverted

# Revert down to a specific migration
$ npx sequelize-cli db:migrate:undo:all --to 20240101000000-initial.js

Migration Status

SQL

-- No built-in tracking; use a custom table

SELECT version, name, applied_at
  FROM schema_migrations
  ORDER BY applied_at DESC;

-- version | name           | applied_at
-- 0002    | add_user_age   | 2024-01-15
-- 0001    | initial        | 2024-01-01

Django ORM

$ python manage.py showmigrations myapp

# myapp
#  [X] 0001_initial
#  [X] 0002_user_age
#  [ ] 0003_user_avatar  ← pending

$ python manage.py showmigrations --plan

# [X]  myapp.0001_initial
# [X]  myapp.0002_user_age
# [ ]  myapp.0003_user_avatar

SQLAlchemy

$ alembic current

# ae1027a6acf (head)

$ alembic history --verbose

# Rev: ae10 (head)
# Parent: 5ba1
# Path: alembic/versions/ae10_add_user_age.py
#     add user age
#
# Rev: 5ba1
# Parent: <base>
# Path: alembic/versions/5ba1_initial.py
#     initial

SQLModel

$ alembic current

# ae1027a6acf (head)

$ alembic history --verbose

# Rev: ae10 (head)
# Parent: 5ba1
# Path: alembic/versions/ae10_add_user_age.py
#     add user age
#
# Rev: 5ba1
# Parent: <base>
# Path: alembic/versions/5ba1_initial.py
#     initial

Tortoise ORM

$ tortoise history

# models
#  [X] 0001_initial
#  [X] 0002_add_user_age

Peewee

# No built-in migration status command in Peewee.
# Common workaround: track applied files in a custom
# migrations table and query it.
$ psql "$DATABASE_URL" -c "SELECT * FROM migrations ORDER BY id;"

Prisma

$ npx prisma migrate status

# Status
# 2 migrations found in prisma/migrations
#
# ✔ 20240101_initial (applied)
# ✔ 20240115_add_user_age (applied)
#
# Database schema is up to date!

Drizzle

$ npx drizzle-kit check

# [✓] 0000_initial.sql
# [✓] 0001_add_user_age.sql
# No pending migrations

Kysely

# Show executed and pending migrations
$ bun run migrate:status

# [X] 20240101_init
# [X] 20240115_add_user_age
# [ ] 20240201_add_user_avatar  ← pending

TypeORM

$ npx typeorm migration:show -d data-source.ts

# [X] Initial1234567880
# [X] AddUserAge1234567890
# [ ] AddUserAvatar1234567900  ← pending

MikroORM

$ npx mikro-orm migration:list

# [X] Migration20240101000000
# [X] Migration20240115000000

$ npx mikro-orm migration:pending

# [ ] Migration20240201000000  ← pending

Sequelize

$ npx sequelize-cli db:migrate:status

# up 20240101000000-initial.js
# up 20240115000000-add-user-age.js
# down 20240201000000-add-user-avatar.js  ← pending