Skip to content

Migrations

Every PlanetScale database and branch resource — PostgresDatabase, PostgresBranch, MySQLDatabase, MySQLBranch — accepts a a migrations prop: a folder of .sql files applied in order as part of every deploy. No separate migration step, no long-lived migration user — alchemy mints a short-lived credential for the run and deletes it afterwards.

On a database, migrations run against the default branch:

const db = yield* Planetscale.PostgresDatabase("MyDb", {
clusterSize: "PS_10",
migrations: "./migrations/postgres",
});

On a branch, they run against that branch itself — the shape used in branch-per-PR workflows, where each preview stage forks a branch and migrates it independently:

const branch = yield* Planetscale.PostgresBranch("app-branch", {
database: db,
migrations: "./migrations",
});

Pairing this with a Drizzle.Schema resource closes the loop: the schema resource regenerates pending migration SQL from your TypeScript schema, and the branch — wired to its out output — applies whatever is new, in one deploy:

const schema = yield* Drizzle.Schema("app-schema", {
schema: "./src/schema.ts",
out: "./migrations",
});
const branch = yield* Planetscale.PostgresBranch("app-branch", {
database,
migrations: schema,
});

Files are discovered recursively under the migrations directory and sorted by their numeric prefix (0001_init.sql, 0002_users.sql, … — Drizzle’s timestamp prefixes work too), falling back to name order for files without one. Each file’s contents are SHA-256 hashed and the hashes are persisted in the resource’s state (migrationsHashes), so adding a migration file — or editing an existing one — is what marks the resource for an update on the next deploy. When nothing changed, the migration step is skipped.

Applied migrations are recorded in Alchemy’s __alchemy_migrations table (created automatically), one row per file:

CREATE TABLE IF NOT EXISTS "__alchemy_migrations" (
id SERIAL PRIMARY KEY,
hash text NOT NULL,
created_at bigint,
name text,
applied_at timestamp with time zone DEFAULT now()
);

(The MySQL flavor is the same shape with MySQL types.) On each deploy, files whose names already appear in the table are skipped.

A database previously migrated with drizzle-kit or Prisma is adopted automatically: its old tracking table’s history is copied in once and the old table is frozen. Use migrations: { dir, table } to put the bookkeeping in a differently-named table:

const db = yield* Planetscale.PostgresDatabase("MyDb", {
clusterSize: "PS_10",
migrations: { dir: "./migrations/postgres", table: "my_migrations" },
});

Both engines follow the same flow — mint a temporary credential, apply pending files, record them, delete the credential — but the details differ:

Postgres connects with a temporary role inheriting postgres (10-minute TTL, deleted after the run — the TTL bounds the orphan window if deletion hiccups). Each migration file runs inside a transaction together with its bookkeeping INSERT, so a failing statement rolls the whole file back and fails the deploy; migrations that already committed stay applied. Fix the file and re-deploy — the run resumes from the failed migration.

MySQL (Vitess) connects with a temporary admin password (same 10-minute TTL). Files are split on Drizzle’s --> statement-breakpoint marker — Vitess’s parser rejects the --> token, so alchemy strips the breakpoints and runs each statement individually. MySQL DDL commits implicitly, so a failing multi-statement migration can leave earlier statements applied; prefer one schema change per migration file.

importFiles lists additional .sql files to apply after migrations. Paths are resolved relative to the working directory:

const db = yield* Planetscale.PostgresDatabase("MyDb", {
clusterSize: "PS_10",
migrations: "./migrations/postgres",
importFiles: ["./seed/postgres.sql"],
});

Unlike migrations, import files are not recorded in the tracking table. Each file is content-hashed and re-applied whenever its contents change, so write them to be safe to run more than once (INSERT ... ON CONFLICT DO NOTHING and the like). Files whose hashes match the previous deploy are skipped.

Branches also support PlanetScale-native seeding at creation time: seedData: "last_successful_backup" restores the last successful backup’s schema and data into the new branch, and backupId restores a specific backup. Both are ignored if the branch already exists.

const branch = yield* Planetscale.PostgresBranch("app-branch", {
database: db,
seedData: "last_successful_backup",
});

Guides:

Related:

  • Postgres — databases, branches, and roles.
  • MySQL — the Vitess-backed family.

Reference: