Guides / Upgrading
Upgrading
What happens to your schema when a new version adds a column, on each database path.
0.3.0 to 0.4.0
Four things to do, in this order:
- Secrets.
secretandcron.secretmust be at least 16 characters and not a placeholder, or startup throws. Generate real ones before deploying. - Schema.
notification_preferencegainsupdated_at.planPostgresMigrationemits theALTER TABLE ... ADD COLUMN IF NOT EXISTSfor it (verified against a 0.3.0-shaped table);drizzle-kitdiffs it; MongoDB needs nothing. Rows written before the upgrade have no value until they are next saved, so the unsubscribe freshness check does not apply to them yet. - Tokens. Signing keys are now derived per purpose, so every token issued by 0.3.0 stops verifying. Unsubscribe links in already-sent email will return 400; re-issue them.
- Plugins and clients.
PluginInitContext.secretis gone; usectx.sign(). Any client that POSTs withoutContent-Type: application/json, or from another origin, now gets 415 or 403. The shipped clients already send both correctly; addtrustedOriginsif your API lives on a different origin from the page.
Which path you're on
| You bootstrapped with | To upgrade |
|---|---|
createSchema() + drizzle-kit | drizzle-kit diffs it for you — nothing to do here |
renderPostgresDdl() | planPostgresMigration() — see below |
createMongoIndexes() | Rerun it. createIndex is idempotent and additive. |
renderPostgresDdl cannot upgrade you. It emits CREATE TABLE IF NOT EXISTS, correct exactly
once. Rerunning it on an existing database does nothing at all — a version that adds a column
would otherwise leave that column silently missing until something hits it at runtime.
planPostgresMigration
import { INTROSPECT_SQL, planPostgresMigration, coreSchema } from "easy-ping/schema";
const plan = await planPostgresMigration(
async () =>
(await sql.unsafe(INTROSPECT_SQL)).map((row) => ({
table: row.table_name,
column: row.column_name,
type: row.data_type,
nullable: row.is_nullable === "YES",
})),
coreSchema,
);
for (const statement of plan.statements) await sql.unsafe(statement);
if (plan.unsupported.length) console.warn(plan.unsupported);
It introspects the live schema, diffs it against the declaration, and emits only what's missing.
Run the same call for any plugin's schema too — push({...}).schema, preferences().schema.
What it will and won't do
Additive only, deliberately. It adds missing columns and indexes, and creates tables that
don't exist yet. It never drops a column, never changes a type, and never touches a column it
doesn't declare — each of those lands in plan.unsupported as a message for a human, because
they're destructive and context-dependent in ways a generic tool shouldn't guess at.
One behavior worth knowing before you run it against a populated table: a required field with
no default is added nullable, because NOT NULL would abort against existing rows. That case
is also reported in unsupported — backfill the column, then ALTER ... SET NOT NULL yourself.