Schema Migration

How to evolve your Grove modules over time using upcast declarations, event versioning, and database migration strategies.

Prerequisites: Versioning & Migration, Database Backends. What you'll learn: How to migrate schemas without data loss.

The Challenge

In Grove, events are immutable. When you change an event's shape (add a field, rename a field, change a type), old events stored in the database still have the original shape. You need a strategy to read old events with new code.

Approach 1: Upcast Declarations

The primary migration mechanism. Define upcasts that transform old event versions into new ones:

event order_placed v1 {
  customer_id: Id
  total: Decimal
}

event order_placed v2 {
  customer_id: Id
  total: Decimal
  currency: String
}

upcast event order_placed v1 -> v2 {
  return {
    customer_id: event.customer_id,
    total: event.total,
    currency: "USD"
  }
}

When the runtime replays v1 events, it automatically upcasts them to v2 before applying to the record. The stored data is unchanged -- the transformation happens at read time.

Action Upcasts

Similarly, action upcasts transform old request formats:

action create v1 { title: String ... }
action create v2 { title: String, priority: Int ... }

upcast action create v1 -> v2 {
  return {
    title: params.title,
    priority: 1
  }
}

Clients sending v1 requests are automatically upgraded to v2 processing.

Approach 2: Additive Changes

Some changes don't require upcasts:

ChangeRequires Upcast?
Add optional field to eventNo -- defaults to null
Add field with default valueNo -- default applies
Add new eventNo -- old events unaffected
Add new actionNo -- new entry point
Change required field to optionalMaybe -- depends on usage
Add required fieldYes -- old events lack it
Remove fieldYes -- old events have it
Rename fieldYes -- old and new names

Approach 3: Database DDL

For the read-side database schema, use grove-cli ddl to generate DDL statements:

manzano ddl examples/apps/todo --backend sqlite
manzano ddl examples/apps/todo --backend postgres

This generates CREATE TABLE statements matching your current record shape. For schema updates:

  1. Generate new DDL from the updated module
  2. Diff against the existing schema
  3. Apply ALTER TABLE statements to add new columns
  4. The event store tables don't change -- events are stored as JSON

Migration Workflow

  1. Write the new version -- Add v2 declarations alongside v1
  2. Write upcasts -- upcast event name v1 -> v2 { ... }
  3. Run checker -- grove-cli check validates upcast compatibility
  4. Test both versions -- Write tests for v1 input (auto-upcasted) and v2 input
  5. Deploy -- The runtime handles v1 events transparently

Best Practices

See Also