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:

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

ChangeResult on existing rows
Add a tableAllowed.
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 requiredRejected 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 defaultAllowed. Rows without a stored value read the new default.
Remove a fieldAllowed. Values already stored are not erased.
Add, change, or remove an indexAllowed. Indexes are rebuilt from the rows.
Remove a tableAllowed, and every row in that table is deleted. This cannot be undone.
Rename a tableSame as removing the old table and adding a new one: the old rows are deleted.
Rename a fieldSame as removing the old field and adding a new one: rows do not carry the value over.

Also:

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 callReplace with
A database call used without awaitawait 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:

// 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:

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.