一键导入
source-command-convex-migration-helper
Guide for safely migrating Convex schema changes - adding fields, changing types, renaming, and batch processing
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Guide for safely migrating Convex schema changes - adding fields, changing types, renaming, and batch processing
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
Set up Convex authentication with proper user management, identity mapping, and access control patterns. Use when implementing auth flows.
Guide to using Convex components for feature encapsulation. Learn about sibling components, creating your own, and when to use components vs monolithic code.
Discover and use convex-helpers utilities for relationships, filtering, sessions, custom functions, and more. Use when you need pre-built Convex patterns.
Create Convex queries, mutations, and actions with proper validation, authentication, and error handling. Use when implementing new API endpoints.
Plan and execute Convex schema migrations safely, including adding fields, creating tables, and data transformations. Use when schema changes affect existing data.
Design and generate Convex database schemas with proper validation, indexes, and relationships. Use when creating schema.ts or modifying table definitions.
基于 SOC 职业分类
| name | source-command-convex-migration-helper |
| description | Guide for safely migrating Convex schema changes - adding fields, changing types, renaming, and batch processing |
Use this skill when the user asks to run the migrated source command convex-migration-helper.
Safe patterns for evolving your Convex schema without data loss.
Safe (no migration needed):
Breaking (migration required):
Never add a required field directly. Follow this 3-step process:
// convex/schema.ts
tasks: defineTable({
title: v.string(),
priority: v.optional(
v.union(
// Start as optional
v.literal("low"),
v.literal("medium"),
v.literal("high"),
),
),
});
// convex/migrations.ts
import { internalMutation } from "./_generated/server";
import { v } from "convex/values";
export const backfillPriority = internalMutation({
args: {},
returns: v.number(),
handler: async (ctx) => {
const tasks = await ctx.db
.query("tasks")
.filter((q) => q.eq(q.field("priority"), undefined))
.take(100); // Process in batches
for (const task of tasks) {
await ctx.db.patch(task._id, { priority: "medium" });
}
if (tasks.length === 100) {
// More to process - schedule next batch
await ctx.scheduler.runAfter(0, internal.migrations.backfillPriority, {});
}
return tasks.length;
},
});
After all data is backfilled, change the schema:
tasks: defineTable({
title: v.string(),
priority: v.union(
// Now required
v.literal("low"),
v.literal("medium"),
v.literal("high"),
),
});
Example: changing status from string to enum.
tasks: defineTable({
status: v.string(), // Old field
statusEnum: v.optional(
v.union(
// New field
v.literal("active"),
v.literal("completed"),
),
),
});
Update all mutations to write both fields:
export const updateStatus = mutation({
args: {
taskId: v.id("tasks"),
status: v.union(v.literal("active"), v.literal("completed")),
},
handler: async (ctx, args) => {
await ctx.db.patch(args.taskId, {
status: args.status, // Old field
statusEnum: args.status, // New field
});
},
});
export const migrateStatus = internalMutation({
args: {},
returns: v.number(),
handler: async (ctx) => {
const tasks = await ctx.db
.query("tasks")
.filter((q) => q.eq(q.field("statusEnum"), undefined))
.take(100);
for (const task of tasks) {
const mapped = task.status === "done" ? "completed" : "active";
await ctx.db.patch(task._id, { statusEnum: mapped });
}
if (tasks.length === 100) {
await ctx.scheduler.runAfter(0, internal.migrations.migrateStatus, {});
}
return tasks.length;
},
});
Once backfill is complete, update queries to read from statusEnum, remove the old status field from schema, and rename if desired.
Same as changing type - add new field, dual-write, backfill, switch reads, remove old.
For any migration that processes large amounts of data:
export const batchMigration = internalMutation({
args: {
cursor: v.optional(v.string()),
processed: v.optional(v.number()),
},
returns: v.object({
processed: v.number(),
done: v.boolean(),
}),
handler: async (ctx, args) => {
const batchSize = 100;
let processed = args.processed ?? 0;
const results = await ctx.db
.query("tableName")
.paginate({ numItems: batchSize, cursor: args.cursor ?? null });
for (const doc of results.page) {
// Your migration logic here
await ctx.db.patch(doc._id, {
/* changes */
});
processed++;
}
if (!results.isDone) {
await ctx.scheduler.runAfter(0, internal.migrations.batchMigration, {
cursor: results.continueCursor,
processed,
});
}
return { processed, done: results.isDone };
},
});
import { convexTest } from "convex-test";
import { describe, it, expect } from "vitest";
import { internal } from "./_generated/api";
import schema from "./schema";
import { modules } from "./test.setup";
describe("migrations", () => {
it("should backfill priority field", async () => {
const t = convexTest(schema, modules);
// Insert test data without the new field
await t.run(async (ctx) => {
await ctx.db.insert("tasks", { title: "Test", status: "active" });
});
// Run migration
await t.mutation(internal.migrations.backfillPriority, {});
// Verify
const tasks = await t.run(async (ctx) => {
return await ctx.db.query("tasks").collect();
});
expect(tasks[0].priority).toBe("medium");
});
});
ctx.scheduler.runAfter(0, internal...)internal.* functions for migrationsconvex-test before running on productionnpx convex codegen after schema changes