Ecto Migrator
Generating Migrations
From Natural Language
Parse the user's description and generate a migration file. Common patterns:
| User Says | Migration Action |
|---|
| "Create users table with email and name" | INLINECODE0 with columns |
| "Add phone to users" |
alter table(:users), add :phone |
| "Make email unique on users" |
create unique_index(:users, [:email]) |
| "Add tenant_id to all tables" | Multiple
alter table with index |
| "Rename status to state on orders" |
rename table(:orders), :status, to: :state |
| "Remove the legacy_id column from users" |
alter table(:users), remove :legacy_id |
| "Add a check constraint on orders amount > 0" |
create constraint(:orders, ...) |
File Naming
CODEBLOCK0
Name conventions: create_<table>, add_<column>_to_<table>, create_<table>_<column>_index, alter_<table>_add_<columns>.
Migration Template
CODEBLOCK1
Column Types
See references/column-types.md for complete type mapping and guidance.
Key decisions:
- - IDs: Use
:binary_id (UUID) — set primary_key: false on table, add :id manually. - Money: Use
:integer (cents) or :decimal — never :float. - Timestamps: Always
timestamps(type: :utc_datetime_usec). - Enums: Use
:string with app-level Ecto.Enum — avoid Postgres enums (hard to migrate). - JSON: Use
:map (maps to jsonb). - Arrays: Use
{:array, :string} etc.
Index Strategies
See references/index-patterns.md for detailed index guidance.
When to Add Indexes
Always index:
- - Foreign keys (
_id columns) - INLINECODE24 (first column in composite indexes)
- Columns used in
WHERE clauses - Columns used in INLINECODE26
- Unique constraints
Index Types
CODEBLOCK2
Constraints
CODEBLOCK3
References (Foreign Keys)
CODEBLOCK4
| INLINECODE27 | Use When |
|---|
| INLINECODE28 | Child can't exist without parent (memberships, line items) |
| INLINECODE29 |
Child should survive parent deletion (optional association) |
|
:nothing | Handle in application code (default) |
|
:restrict | Prevent parent deletion if children exist |
Multi-Tenant Patterns
Every Table Gets tenant_id
CODEBLOCK5
Adding tenant_id to Existing Tables
CODEBLOCK6
Data Migrations
Rule: Never mix schema changes and data changes in the same migration.
Safe Data Migration Pattern
CODEBLOCK7
Batched Data Migration (large tables)
CODEBLOCK8
Reversible vs Irreversible
Reversible (use change)
These are auto-reversible:
- -
create table ↔ INLINECODE34 - INLINECODE35 ↔ INLINECODE36
- INLINECODE37 ↔ INLINECODE38
- INLINECODE39 ↔ INLINECODE40
Irreversible (use up/down)
Must define both directions:
- -
modify column type — Ecto can't infer the old type - INLINECODE44 raw SQL
- Data backfills
- Dropping columns with data
CODEBLOCK9
Using modify with from:
Phoenix 1.7+ supports from: for reversible modify:
CODEBLOCK10
PostgreSQL Extensions
CODEBLOCK11
Enum Types (PostgreSQL native — use sparingly)
Prefer Ecto.Enum with :string columns. If you must use Postgres enums:
CODEBLOCK12
Warning: Adding values to Postgres enums requires ALTER TYPE ... ADD VALUE which cannot run inside a transaction. Prefer :string + Ecto.Enum.
Checklist
- - [ ] Primary key:
primary_key: false + INLINECODE55 - [ ]
null: false on required columns - [ ] INLINECODE57
- [ ] Foreign keys with appropriate INLINECODE58
- [ ] Index on every foreign key column
- [ ]
tenant_id indexed (composite with lookup fields) - [ ] Unique constraints where needed
- [ ] Concurrent indexes in separate migration with INLINECODE60
- [ ] Data migrations in separate files from schema migrations
Ecto 迁移器
生成迁移
从自然语言生成
解析用户描述并生成迁移文件。常见模式:
| 用户描述 | 迁移操作 |
|---|
| 创建包含邮箱和姓名的用户表 | create table(:users) 并添加列 |
| 向用户表添加手机号 |
alter table(:users), add :phone |
| 设置用户邮箱为唯一 | create unique_index(:users, [:email]) |
| 向所有表添加租户ID | 多个 alter table 并添加索引 |
| 将订单表中的status重命名为state | rename table(:orders), :status, to: :state |
| 从用户表中删除legacy
id列 | alter table(:users), remove :legacyid |
| 为订单表添加金额大于0的检查约束 | create constraint(:orders, ...) |
文件命名
bash
mix ecto.gen.migration <名称>
生成: priv/repo/migrations/YYYYMMDDHHMMSS_<名称>.exs
命名规范:create<表名>、add<列名>to<表名>、create<表名><列名>index、alter<表名>add<列名>。
迁移模板
elixir
defmodule MyApp.Repo.Migrations.CreateUsers do
use Ecto.Migration
def change do
create table(:users, primary_key: false) do
add :id, :binaryid, primarykey: true
add :email, :string, null: false
add :name, :string, null: false
add :role, :string, null: false, default: member
add :metadata, :map, default: %{}
add :tenantid, :binaryid, null: false
add :teamid, references(:teams, type: :binaryid, ondelete: :deleteall)
timestamps(type: :utcdatetimeusec)
end
create uniqueindex(:users, [:tenantid, :email])
create index(:users, [:tenant_id])
create index(:users, [:team_id])
end
end
列类型
完整类型映射和指南请参见 references/column-types.md。
关键决策:
- - ID:使用 :binaryid(UUID)— 在表上设置 primarykey: false,手动添加 :id。
- 金额:使用 :integer(分)或 :decimal — 绝不使用 :float。
- 时间戳:始终使用 timestamps(type: :utcdatetimeusec)。
- 枚举:使用 :string 配合应用层 Ecto.Enum — 避免使用 PostgreSQL 枚举(难以迁移)。
- JSON:使用 :map(映射为 jsonb)。
- 数组:使用 {:array, :string} 等。
索引策略
详细索引指南请参见 references/index-patterns.md。
何时添加索引
始终为以下内容添加索引:
- - 外键(id 列)
- tenantid(复合索引中的第一列)
- WHERE 子句中使用的列
- ORDER BY 中使用的列
- 唯一约束
索引类型
elixir
标准 B-tree
create index(:users, [:tenant_id])
唯一索引
create unique
index(:users, [:tenantid, :email])
部分索引(条件索引)
create index(:orders, [:status], where: status != completed, name: :orders
activestatus_idx)
JSONB 的 GIN 索引
create index(:events, [:metadata], using: :gin)
数组列的 GIN 索引
create index(:posts, [:tags], using: :gin)
复合索引
create index(:orders, [:tenant
id, :status, :insertedat])
并发索引(不锁表 — 在单独迁移中使用)
@disable
ddltransaction true
@disable
migrationlock true
def change do
create index(:users, [:email], concurrently: true)
end
约束
elixir
检查约束
create constraint(:orders, :amount
mustbe_positive, check: amount > 0)
排除约束(需要 btree_gist 扩展)
execute CREATE EXTENSION IF NOT EXISTS btree_gist,
create constraint(:reservations, :no
overlappingbookings,
exclude: ~s|gist (room
id WITH =, tstzrange(startsat, ends_at) WITH &&)|
)
唯一约束(大多数情况下与 unique_index 相同)
create unique_index(:accounts, [:slug])
引用(外键)
elixir
add :userid, references(:users, type: :binaryid, ondelete: :deleteall), null: false
add :teamid, references(:teams, type: :binaryid, ondelete: :nilifyall)
add :parentid, references(:categories, type: :binaryid, on_delete: :nothing)
| ondelete | 使用场景 |
|---|
| :deleteall | 子记录不能脱离父记录存在(成员关系、订单明细) |
| :nilify_all |
子记录应在父记录删除后继续存在(可选关联) |
| :nothing | 在应用代码中处理(默认值) |
| :restrict | 如果存在子记录则阻止父记录删除 |
多租户模式
每个表都包含 tenant_id
elixir
def change do
create table(:items, primary_key: false) do
add :id, :binaryid, primarykey: true
add :name, :string, null: false
add :tenantid, :binaryid, null: false
timestamps(type: :utcdatetimeusec)
end
# 始终以 tenant_id 作为第一列的复合索引
create index(:items, [:tenant_id])
create uniqueindex(:items, [:tenantid, :name])
end
向现有表添加 tenant_id
elixir
def change do
alter table(:items) do
add :tenantid, :binaryid
end
# 在单独的数据迁移中进行回填,然后:
# alter table(:items) do
# modify :tenantid, :binaryid, null: false
# end
end
数据迁移
规则:切勿在同一迁移中混合模式变更和数据变更。
安全的数据迁移模式
elixir
defmodule MyApp.Repo.Migrations.BackfillUserRoles do
use Ecto.Migration
# 不要使用模式模块 — 它们可能在此迁移运行后发生变化
def up do
execute
UPDATE users SET role = member WHERE role IS NULL
end
def down do
# 数据迁移可能不可逆
:ok
end
end
批量数据迁移(大表)
elixir
def up do
execute
UPDATE users SET role = member
WHERE id IN (
SELECT id FROM users WHERE role IS NULL LIMIT 10000
)
# 对于非常大的表,改用 Task 或 Oban 任务
end
可逆与不可逆
可逆(使用 change)
以下操作可自动逆反:
- - create table ↔ drop table
- add column ↔ remove column
- create index ↔ drop index
- rename ↔ rename
不可逆(使用 up/down)
必须定义两个方向:
- - modify 列类型 — Ecto 无法推断旧类型
- execute 原始 SQL
- 数据回填
- 删除包含数据的列
elixir
def up do
alter table(:users) do
modify :email, :citext, from: :string # from: 有助于可逆性
end
end
def down do
alter table(:users) do
modify :email, :string, from: :citext
end
end
使用带 from: 的 modify
Phoenix 1.7+ 支持使用 from: 实现可逆的 modify:
elixir
def change do
alter table(:users) do
modify :email, :citext, null: false, from: {:string, null: true}
end
end
PostgreSQL 扩展
elixir
def change do
execute CREATE EXTENSION IF NOT EXISTS citext, DROP EXTENSION