1---
2title: Migrations
3description: Apply SQL migrations and seed data to PlanetScale databases and branches as part of every deploy — ordered, hashed, tracked, and run over short-lived credentials.
4---
5
6Every PlanetScale database and branch resource — `PostgresDatabase`,
7`PostgresBranch`, `MySQLDatabase`, `MySQLBranch` — accepts a
8a `migrations` prop: a folder of `.sql` files applied in order as part of
9every deploy. No separate migration step, no long-lived migration
10user — alchemy mints a short-lived credential for the run and deletes
11it afterwards.
12
13## Database vs branch
14
15On a database, migrations run against the default branch:
16
17```typescript
18const db = yield* Planetscale.PostgresDatabase("MyDb", {
19 clusterSize: "PS_10",
20 migrations: "./migrations/postgres",
21});
22```
23
24On a branch, they run against that branch itself — the shape used in
25branch-per-PR workflows, where each preview stage forks a branch and
26migrates it independently:
27
28```typescript
29const branch = yield* Planetscale.PostgresBranch("app-branch", {
30 database: db,
31 migrations: "./migrations",
32});
33```
34
35Pairing this with a `Drizzle.Schema` resource closes the loop: the
36schema resource regenerates pending migration SQL from your
37TypeScript schema, and the branch — wired to its `out` output —
38applies whatever is new, in one deploy:
39
40```typescript
41const schema = yield* Drizzle.Schema("app-schema", {
42 schema: "./src/schema.ts",
43 out: "./migrations",
44});
45
46const branch = yield* Planetscale.PostgresBranch("app-branch", {
47 database,
48 migrations: schema,
49});
50```
51
52## Ordering and hashing
53
54Files are discovered recursively under the migrations directory and sorted by
55their numeric prefix (`0001_init.sql`, `0002_users.sql`, … — Drizzle's
56timestamp prefixes work too), falling back to name order for files
57without one. Each file's contents are SHA-256 hashed and the hashes
58are persisted in the resource's state (`migrationsHashes`), so adding
59a migration file — or editing an existing one — is what marks the
60resource for an update on the next deploy. When nothing changed, the
61migration step is skipped.
62
63## The tracking table
64
65Applied migrations are recorded in Alchemy's `__alchemy_migrations`
66table (created automatically), one row per file:
67
68```sql
69CREATE TABLE IF NOT EXISTS "__alchemy_migrations" (
70 id SERIAL PRIMARY KEY,
71 hash text NOT NULL,
72 created_at bigint,
73 name text,
74 applied_at timestamp with time zone DEFAULT now()
75);
76```
77
78(The MySQL flavor is the same shape with MySQL types.) On each
79deploy, files whose names already appear in the table are skipped.
80
81A database previously migrated with drizzle-kit or Prisma is
82[adopted automatically](/sql/effect-sql/migrations#adopting-an-existing-database):
83its old tracking table's history is copied in once and the old table
84is frozen. Use `migrations: { dir, table }` to put the bookkeeping in
85a differently-named table:
86
87```typescript
88const db = yield* Planetscale.PostgresDatabase("MyDb", {
89 clusterSize: "PS_10",
90 migrations: { dir: "./migrations/postgres", table: "my_migrations" },
91});
92```
93
94## Engine differences
95
96Both engines follow the same flow — mint a temporary credential,
97apply pending files, record them, delete the credential — but the
98details differ:
99
100**Postgres** connects with a temporary role inheriting `postgres`
101(10-minute TTL, deleted after the run — the TTL bounds the orphan
102window if deletion hiccups). Each migration file runs inside a
103transaction together with its bookkeeping `INSERT`, so a failing
104statement rolls the whole file back and fails the deploy; migrations
105that already committed stay applied. Fix the file and re-deploy — the
106run resumes from the failed migration.
107
108**MySQL (Vitess)** connects with a temporary `admin` password (same
10910-minute TTL). Files are split on Drizzle's `--> statement-breakpoint`
110marker — Vitess's parser rejects the `-->` token, so alchemy strips
111the breakpoints and runs each statement individually. MySQL DDL
112commits implicitly, so a failing multi-statement migration can leave
113earlier statements applied; prefer one schema change per migration
114file.
115
116## Seed data
117
118`importFiles` lists additional `.sql` files to apply after
119migrations. Paths are resolved relative to the working directory:
120
121```typescript
122const db = yield* Planetscale.PostgresDatabase("MyDb", {
123 clusterSize: "PS_10",
124 migrations: "./migrations/postgres",
125 importFiles: ["./seed/postgres.sql"],
126});
127```
128
129Unlike migrations, import files are not recorded in the tracking
130table. Each file is content-hashed and re-applied whenever its
131contents change, so write them to be safe to run more than once
132(`INSERT ... ON CONFLICT DO NOTHING` and the like). Files whose
133hashes match the previous deploy are skipped.
134
135Branches also support PlanetScale-native seeding at creation time:
136`seedData: "last_successful_backup"` restores the last successful
137backup's schema and data into the new branch, and `backupId` restores
138a specific backup. Both are ignored if the branch already exists.
139
140```typescript
141const branch = yield* Planetscale.PostgresBranch("app-branch", {
142 database: db,
143 seedData: "last_successful_backup",
144});
145```
146
147## Where next
148
149Guides:
150
151- [Add Drizzle ORM](/cloudflare/data/drizzle) — typed schemas and
152 generated migrations.
153- [Branch from a shared database](/cloudflare/data/branch-from-shared-database)
154 — branch-per-PR preview environments.
155
156Related:
157
158- [Postgres](/planetscale/data/postgres) — databases, branches, and roles.
159- [MySQL](/planetscale/data/mysql) — the Vitess-backed family.
160
161Reference:
162
163- [PostgresDatabase](/providers/planetscale/postgres/postgresdatabase) ·
164 [PostgresBranch](/providers/planetscale/postgres/postgresbranch)
165- [MySQLDatabase](/providers/planetscale/mysql/mysqldatabase) ·
166 [MySQLBranch](/providers/planetscale/mysql/mysqlbranch)