clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/184-task-notice-migration
lofi / docs / examples / schema-evolution.md
schema-evolution.md 2.6K rendered
1# Schema evolution23A deployed schema changes with a migration: a bidirectional lens between two schema versions. Rows4written under the old schema read correctly through the new one, and — because the backward5direction is generated automatically — clients still on the old schema keep reading rows written6under the new one. The whole surface below is verified against a real server in7`package/schema/migration_test.ts`.89## Two versions of the schema1011The migration names both versions explicitly, so keep the outgoing version around as a value:1213```ts14import { s } from "@nzip/lofi/schema";1516const v1 = s.defineSchema({17 todos: s.table({ title: s.string(), done: s.boolean(), detail: s.string() }),18 projects: s.table({ name: s.string() }),19});2021const v2 = s.defineSchema({22 todos: s.table({23 title: s.string(),24 note: s.string(),25 priority: s.enum("low", "medium", "high"),26 }),27 workspaces: s.table({ name: s.string() }),28});2930export const app = s.defineApp(v2);31```3233## Describe the change, both directions come free3435```ts36export const migration = s.defineMigration({37 from: v1,38 to: v2,39 renameTables: { workspaces: s.renameTableFrom("projects") },40 migrate: {41 todos: {42 priority: s.add.enum("low", "medium", "high", { default: "medium" }),43 note: s.renameFrom("detail"),44 done: s.drop.boolean({ backwardsDefault: false }),45 },46 },47});48```4950Every added or removed table and column must be covered — `renameTables`/`createTables`/51`dropTables` at the table level, `s.add.*`, `s.drop.*`, and `s.renameFrom` at the column level — and52the definition fails to type-check until it is. `s.add.*` requires a `default` (what old rows show53under the new schema); `s.drop.*` requires a `backwardsDefault` (what old clients see on rows54written without the column).5556Verified behavior across the deployment:5758- A v1 row reads under v2 with `note` carrying the old `detail` value, `priority` defaulted to59 `"medium"`, no `done`, and `projects` rows answering under `workspaces`.60- A v2-written row reads under v1 handles with the rename reversed and `done: false` from the61 backwards default; the v2-only `priority` column does not leak.62- Deploying a changed schema **without** its migration publishes nothing usable: the deploy reports63 `migration: { status: "missing" }` and withholds the permissions, so clients never see a schema64 they cannot read data through.6566## Day to day6768Use `deno task schema:validate` while editing. For a deployed app, generate the migration stub with69`deno task migrations:create`, fill in the ops as above, review it, then publish with70`migrations:push` and `schema:deploy` under the intended managed configuration.