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:
| Change | Requires Upcast? |
|---|---|
| Add optional field to event | No -- defaults to null |
| Add field with default value | No -- default applies |
| Add new event | No -- old events unaffected |
| Add new action | No -- new entry point |
| Change required field to optional | Maybe -- depends on usage |
| Add required field | Yes -- old events lack it |
| Remove field | Yes -- old events have it |
| Rename field | Yes -- 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:
- Generate new DDL from the updated module
- Diff against the existing schema
- Apply
ALTER TABLEstatements to add new columns - The event store tables don't change -- events are stored as JSON
Migration Workflow
- Write the new version -- Add
v2declarations alongsidev1 - Write upcasts --
upcast event name v1 -> v2 { ... } - Run checker --
grove-cli checkvalidates upcast compatibility - Test both versions -- Write tests for v1 input (auto-upcasted) and v2 input
- Deploy -- The runtime handles v1 events transparently
Best Practices
- Never modify existing versions -- Always create a new version. Old events depend on the original shape.
- Chain upcasts -- v1 -> v2 -> v3 is applied sequentially. Each step is simple.
- Keep defaults sensible -- When adding fields, the upcast default should be a reasonable business value.
- Test replay -- Write tests that simulate the full event history (v1 events followed by v2 events).
- Version explicitly -- Use explicit
v1,v2tags to make the migration path clear.
See Also
- Versioning & Migration -- Language-level versioning guide
- Database Backends -- DDL generation and backend setup
- Semantic Constraints -- Upcast validation rules