# Schema changes

What happens to existing data when you change the schema and redeploy, and how to move an older fold to the async database API.

## How a schema change is applied

Every deploy carries the full schema. When you redeploy, Fold checks every stored row against the new schema and rebuilds every index, in one step:

- If every row fits, the new version goes live.
- If any row does not fit, the deploy is rejected and the previous version stays live with its data unchanged. The CLI prints the reason:

```txt
The database could not switch to this schema: Missing value for tasks.priority
```

The local dev server applies the same rules when it rebuilds after a file change. If a change does not fit the local data, the rebuild fails, the old build keeps running, and the error appears in `npx @aozumi/fold logs --port 3000`. Restarting the dev server clears local data.

## What is allowed

| Change | Result on existing rows |
|---|---|
| Add a table | Allowed. |
| Add a field with `.optional()` | Allowed. Old rows have no value. |
| Add a field with `.default(value)` | Allowed. Old rows read as `value`. Nothing is rewritten. |
| Add a required field (no default, not optional) | Rejected if the table has rows: `Missing value for <table>.<field>`. Allowed on an empty table. |
| Make an optional field required | Rejected if any row lacks a value, unless you also add `.default(value)`. |
| Change a field's kind, such as `string()` to `number()` | Rejected if any row holds a value of the old kind: `Expected <table>.<field> to be a number.` |
| Change or add a default | Allowed. Rows without a stored value read the new default. |
| Remove a field | Allowed. Values already stored are not erased. |
| Add, change, or remove an index | Allowed. Indexes are rebuilt from the rows. |
| Remove a table | Allowed, and **every row in that table is deleted**. This cannot be undone. |
| Rename a table | Same as removing the old table and adding a new one: the old rows are deleted. |
| Rename a field | Same as removing the old field and adding a new one: rows do not carry the value over. |

Also:

- `userId()` values of guests who have since signed in are updated to the account's user ID during the switch.
- Pagination cursors issued before the switch stop working: `Pagination cursor belongs to a stale schema.` Clients using `usePaginatedQuery` start over.
- Open pages pick up the new version after a redeploy.

## Safe patterns

Add a required field to a table that has data:

1. Add it with `.default(value)`, or with `.optional()` and fill it in.
2. Deploy.
3. If you need a stored value on every row, write a mutation that updates rows a page at a time with `paginate()`, and call it until it reports it is done. Each call can write at most 1,000 rows.

Rename a field without losing data:

1. Add the new field as `.optional()`. Keep the old one.
2. Deploy, then run a mutation that copies the old value into the new field.
3. Switch reads and writes to the new field. Deploy.
4. Remove the old field. Deploy.

Never remove or rename a table that holds data you want to keep.

Before deploying a schema change to a fold that has real users, check it against a copy of the change in local dev with some rows already inserted.

## Older folds with synchronous database calls

Fold's database API is async and index-based. Code written against the older synchronous, scan-based calls must be updated. Anonymous builds reject the old calls in server code, and at runtime they throw.

| Old call | Replace with |
|---|---|
| A database call used without `await` | `await` it, or return it. Every `ctx.db` method returns a `Promise`. |
| `where(...)` (full scan) | Declare `.index(name, fields)` on the table and use `withIndex(name, (q) => q.eq(field, value))`. |
| `orderBy(...)` (in-memory sort) | Use the index order and `.order("asc")` or `.order("desc")`. To sort by a field, put it in an index. |
| `limit(n)` | `.take(n)`, `.first()`, or `.paginate(...)`. |
| `all()` | `.collect()`, `.take(n)`, `.first()`, or `.paginate(...)`. |

Before and after, for a per-user list sorted newest first:

```ts
// Schema: add an index on the field you filter by.
todos: table({ text: string(), ownerId: userId() }).index("by_owner", ["ownerId"])

// Query
todos: query(async (ctx) => {
  const { userId } = ctx.auth.requireIdentity();
  return ctx.db.todos
    .withIndex("by_owner", (q) => q.eq("ownerId", userId))
    .order("desc")
    .collect();
})
```

Prompt to give an agent for this migration:

```txt
Update this fold to the async, index-based Fold database API described at docs.aozumi.dev/fold/database/.
Await every ctx.db call. Replace where/orderBy/limit/all with declared indexes and withIndex, order, take, first, collect, or paginate.
Add an .index(...) for every field used as a filter. Use userId() for owner fields.
Then run `npx @aozumi/fold build --target anonymous` and `npx @aozumi/fold dev`, and fix every error.
```

Anonymous builds also reject any `.all(` call in server code, not only on `ctx.db`. `Promise.all(...)` is fine.
