220 lines
5.0 KiB
Markdown
220 lines
5.0 KiB
Markdown
# Migrations Component Reference
|
|
|
|
Complete guide to the
|
|
[`@convex-dev/migrations`](https://www.convex.dev/components/migrations)
|
|
component for batched, resumable Convex data migrations.
|
|
|
|
## Installation
|
|
|
|
```bash
|
|
npm install @convex-dev/migrations
|
|
```
|
|
|
|
## Setup
|
|
|
|
```typescript
|
|
// convex/convex.config.ts
|
|
import { defineApp } from "convex/server";
|
|
import migrations from "@convex-dev/migrations/convex.config.js";
|
|
|
|
const app = defineApp();
|
|
app.use(migrations);
|
|
export default app;
|
|
```
|
|
|
|
```typescript
|
|
// convex/migrations.ts
|
|
import { Migrations } from "@convex-dev/migrations";
|
|
import { components } from "./_generated/api.js";
|
|
import { DataModel } from "./_generated/dataModel.js";
|
|
|
|
export const migrations = new Migrations<DataModel>(components.migrations);
|
|
```
|
|
|
|
The `DataModel` type parameter is optional but provides type safety for
|
|
migration definitions.
|
|
|
|
## Define a Migration
|
|
|
|
The `migrateOne` function processes a single document. The component handles
|
|
batching and pagination automatically.
|
|
|
|
```typescript
|
|
// convex/migrations.ts
|
|
export const addDefaultRole = migrations.define({
|
|
table: "users",
|
|
migrateOne: async (ctx, user) => {
|
|
if (user.role === undefined) {
|
|
await ctx.db.patch(user._id, { role: "user" });
|
|
}
|
|
},
|
|
});
|
|
```
|
|
|
|
Shorthand: if you return an object, it is applied as a patch automatically.
|
|
|
|
```typescript
|
|
export const clearDeprecatedField = migrations.define({
|
|
table: "users",
|
|
migrateOne: () => ({ legacyField: undefined }),
|
|
});
|
|
```
|
|
|
|
## Run a Migration
|
|
|
|
From the CLI:
|
|
|
|
```bash
|
|
npx convex run migrations:addDefaultRole
|
|
|
|
# Pass --prod to run in production.
|
|
npx convex run migrations:addDefaultRole --prod
|
|
```
|
|
|
|
The migration exported by `migrations.define` is directly callable from the CLI
|
|
or dashboard. You do not need a separate one-off runner for normal single
|
|
migrations.
|
|
|
|
If you want a general-purpose runner that accepts a migration name, define one:
|
|
|
|
```typescript
|
|
export const run = migrations.runner();
|
|
```
|
|
|
|
Then call it with the full function name:
|
|
|
|
```bash
|
|
npx convex run migrations:run '{"fn": "migrations:addDefaultRole"}'
|
|
```
|
|
|
|
Programmatically from another Convex function:
|
|
|
|
```typescript
|
|
await migrations.runOne(ctx, internal.migrations.addDefaultRole);
|
|
```
|
|
|
|
## Run Multiple Migrations in Order
|
|
|
|
For a short ad hoc series, pass `next` when starting the first migration:
|
|
|
|
```bash
|
|
npx convex run migrations:addDefaultRole '{"next":["migrations:clearDeprecatedField","migrations:normalizeEmails"]}'
|
|
```
|
|
|
|
For a reusable series, define a runner:
|
|
|
|
```typescript
|
|
export const runAll = migrations.runner([
|
|
internal.migrations.addDefaultRole,
|
|
internal.migrations.clearDeprecatedField,
|
|
internal.migrations.normalizeEmails,
|
|
]);
|
|
```
|
|
|
|
```bash
|
|
npx convex run migrations:runAll
|
|
```
|
|
|
|
If one fails, it stops and will not continue to the next. Call it again to retry
|
|
from where it left off. Completed migrations are skipped automatically.
|
|
|
|
Programmatically from another Convex function:
|
|
|
|
```typescript
|
|
await migrations.runSerially(ctx, [
|
|
internal.migrations.addDefaultRole,
|
|
internal.migrations.clearDeprecatedField,
|
|
internal.migrations.normalizeEmails,
|
|
]);
|
|
```
|
|
|
|
## Dry Run
|
|
|
|
Test a migration before committing changes:
|
|
|
|
```bash
|
|
npx convex run migrations:addDefaultRole '{"dryRun": true}'
|
|
```
|
|
|
|
This runs one batch and then rolls back, so you can see what it would do without
|
|
changing any data.
|
|
|
|
## Restart a Migration
|
|
|
|
Pass `reset: true` to restart a migration from the beginning:
|
|
|
|
```bash
|
|
npx convex run migrations:addDefaultRole '{"reset": true}'
|
|
```
|
|
|
|
If you specify `next` or run a defined series, `reset: true` resets the cursor
|
|
for all migrations in the group.
|
|
|
|
## Check Migration Status
|
|
|
|
```bash
|
|
npx convex run --component migrations lib:getStatus --watch
|
|
```
|
|
|
|
## Cancel a Running Migration
|
|
|
|
```bash
|
|
npx convex run --component migrations lib:cancel '{"name": "migrations:addDefaultRole"}'
|
|
```
|
|
|
|
Or programmatically:
|
|
|
|
```typescript
|
|
await migrations.cancel(ctx, internal.migrations.addDefaultRole);
|
|
```
|
|
|
|
## Run Migrations on Deploy
|
|
|
|
Chain migration execution after deploying:
|
|
|
|
```bash
|
|
npx convex deploy --cmd 'npm run build' && npx convex run migrations:runAll --prod
|
|
```
|
|
|
|
## Configuration Options
|
|
|
|
### Custom Batch Size
|
|
|
|
If documents are large or the table has heavy write traffic, reduce the batch
|
|
size to avoid transaction limits or OCC conflicts:
|
|
|
|
```typescript
|
|
export const migrateHeavyTable = migrations.define({
|
|
table: "largeDocuments",
|
|
batchSize: 10,
|
|
migrateOne: async (ctx, doc) => {
|
|
// migration logic
|
|
},
|
|
});
|
|
```
|
|
|
|
### Migrate a Subset Using an Index
|
|
|
|
Process only matching documents instead of the full table:
|
|
|
|
```typescript
|
|
export const fixEmptyNames = migrations.define({
|
|
table: "users",
|
|
customRange: (query) => query.withIndex("by_name", (q) => q.eq("name", "")),
|
|
migrateOne: () => ({ name: "<unknown>" }),
|
|
});
|
|
```
|
|
|
|
### Parallelize Within a Batch
|
|
|
|
By default each document in a batch is processed serially. Enable parallel
|
|
processing if your migration logic does not depend on ordering:
|
|
|
|
```typescript
|
|
export const clearField = migrations.define({
|
|
table: "myTable",
|
|
parallelize: true,
|
|
migrateOne: () => ({ optionalField: undefined }),
|
|
});
|
|
```
|