Handle SQL migrations
Mange SQL migrations in a Durable Object.
This example shows how to handle SQL migrations in a Durable Object. To use this example, make sure that the id
of the migrations are sequential.
import { DurableObject } from "cloudflare:workers";
// add your migrations hereconst migrations = [ { id: 1, description: "Create 'users' table", sql: ` CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT NOT NULL ); `, }, { id: 2, description: "Add age column", sql: ` ALTER TABLE users ADD COLUMN age INTEGER;`, },];
// Handles the SQL migrationsasync function runMigrations(storage) { const result = { rowsRead: 0, rowsWritten: 0, };
// fetch the last migration version that was run const currentVersion = (await storage.get("migration")) ?? 0;
// filter out the migrations that have not been run const pendingMigrations = migrations.filter((m) => m.id > currentVersion);
// no migrations to run if (pendingMigrations.length === 0) { return result; }
try { await storage.transaction(async () => { for (let migration of pendingMigrations) { console.log( `Running migration ${migration.id}: ${migration.description}`, ); const cursor = storage.sql.exec(migration.sql); let _ = cursor.toArray(); result.rowsRead += cursor.rowsRead; result.rowsWritten += cursor.rowsWritten; // store the migration version that was run await storage.put("migration", migration.id); } }); return result; } catch (e) { console.error(e); throw new Error("Migration failed"); }}
export class MigrationExampleDO extends DurableObject { storage;
constructor(ctx, env) { super(ctx, env); this.storage = ctx.storage; }
// inserts a user in the user table async insertUser(name) { // run migrations before write await runMigrations(this.storage);
return this.storage.sql.exec( `INSERT INTO users (name) VALUES ('${name}');`, ); }}
export default { /** * This is the standard fetch handler for a Cloudflare Worker * * @param request - The request submitted to the Worker from the client * @param env - The interface to reference bindings declared in wrangler.jsonc * @param ctx - The execution context of the Worker * @returns The response to be sent back to the client */ async fetch(request, env, ctx) { // We will create a `DurableObjectId` using the pathname from the Worker request // This id refers to a unique instance of our 'MigrationExampleDO' class above let id = env.MIGRATION_EXAMPLE_DO.idFromName(new URL(request.url).pathname);
// This stub creates a communication channel with the Durable Object instance // The Durable Object constructor will be invoked upon the first call for a given id let stub = env.MIGRATION_EXAMPLE_DO.get(id);
// Inserts a user into the 'users' table stub.insertUser("John");
return new Response("User inserted successfully", { status: 200 }); },};
import { DurableObject } from "cloudflare:workers";
type SQLMigration = { id: number; // should be sequential description: string; // description of the migration sql: string; // SQL statement to run};
// add your migrations hereconst migrations: SQLMigration[] = [ { id: 1, description: "Create 'users' table", sql: ` CREATE TABLE users ( id INTEGER PRIMARY KEY, name TEXT NOT NULL ); `, }, { id: 2, description: "Add age column", sql: ` ALTER TABLE users ADD COLUMN age INTEGER;`, },];
// Handles the SQL migrationsasync function runMigrations( storage: DurableObjectStorage,): Promise<{ rowsRead: number; rowsWritten: number }> { const result = { rowsRead: 0, rowsWritten: 0, };
// fetch the last migration version that was run const currentVersion = (await storage.get<number>("migration")) ?? 0;
// filter out the migrations that have not been run const pendingMigrations = migrations.filter((m) => m.id > currentVersion);
// no migrations to run if (pendingMigrations.length === 0) { return result; }
try { await storage.transaction(async () => { for (let migration of pendingMigrations) { console.log( `Running migration ${migration.id}: ${migration.description}`, ); const cursor = storage.sql.exec(migration.sql); let _ = cursor.toArray(); result.rowsRead += cursor.rowsRead; result.rowsWritten += cursor.rowsWritten; // store the migration version that was run await storage.put("migration", migration.id); } }); return result; } catch (e) { console.error(e); throw new Error("Migration failed"); }}
export class MigrationExampleDO extends DurableObject<Env> { storage: DurableObjectStorage;
constructor(ctx: DurableObjectState, env: Env) { super(ctx, env); this.storage = ctx.storage; }
// inserts a user in the user table async insertUser(name: string) { // run migrations before write await runMigrations(this.storage);
return this.storage.sql.exec( `INSERT INTO users (name) VALUES ('${name}');`, ); }}
export default { /** * This is the standard fetch handler for a Cloudflare Worker * * @param request - The request submitted to the Worker from the client * @param env - The interface to reference bindings declared in wrangler.jsonc * @param ctx - The execution context of the Worker * @returns The response to be sent back to the client */ async fetch(request, env, ctx): Promise<Response> { // We will create a `DurableObjectId` using the pathname from the Worker request // This id refers to a unique instance of our 'MigrationExampleDO' class above let id: DurableObjectId = env.MIGRATION_EXAMPLE_DO.idFromName( new URL(request.url).pathname, );
// This stub creates a communication channel with the Durable Object instance // The Durable Object constructor will be invoked upon the first call for a given id let stub = env.MIGRATION_EXAMPLE_DO.get(id);
// Inserts a user into the 'users' table stub.insertUser("John");
return new Response("User inserted successfully", { status: 200 }); },} satisfies ExportedHandler<Env>;
Finally, configure your Wrangler file to include a Durable Object binding and migration based on the namespace and class name chosen previously.
{ "main": "src/index.ts", "name": "sql-migration-do", "durable_objects": { "bindings": [ { "name": "MIGRATION_EXAMPLE_DO", "class_name": "MigrationExampleDO" } ] }, "migrations": [ { "tag": "v1", "new_sqlite_classes": [ "MigrationExampleDO" ] } ]}
main = "src/index.ts"name = "sql-migration-do"
[[durable_objects.bindings]]name = "MIGRATION_EXAMPLE_DO"class_name = "MigrationExampleDO"
[[migrations]]tag = "v1"new_sqlite_classes = ["MigrationExampleDO"]