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