140 lines
4.2 KiB
Markdown
140 lines
4.2 KiB
Markdown
---
|
||
title: Version Your Schema with Go Migrations
|
||
impact: HIGH
|
||
impactDescription: Guarantees repeatable, transactional schema evolution and eliminates manual dashboard changes in production
|
||
tags: go, migrations, schema, database, migratecmd, extending
|
||
---
|
||
|
||
## Version Your Schema with Go Migrations
|
||
|
||
PocketBase ships with a `migratecmd` plugin that generates versioned `.go` migration files, applies them automatically on `serve`, and lets you roll back with `migrate down`. Because the files are compiled into your binary, no extra migration tool is needed.
|
||
|
||
**Incorrect (one-off SQL or dashboard changes in production):**
|
||
|
||
```go
|
||
// ❌ Running raw SQL directly at startup without a migration file –
|
||
// the change is applied every restart and has no rollback path.
|
||
app.OnServe().BindFunc(func(se *core.ServeEvent) error {
|
||
_, err := app.DB().NewQuery(
|
||
"ALTER TABLE posts ADD COLUMN summary TEXT DEFAULT ''",
|
||
).Execute()
|
||
return err
|
||
})
|
||
|
||
// ❌ Forgetting to import the migrations package means
|
||
// registered migrations are never executed.
|
||
package main
|
||
|
||
import (
|
||
"github.com/pocketbase/pocketbase"
|
||
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
||
// _ "myapp/migrations" ← omitted: migrations never run
|
||
)
|
||
```
|
||
|
||
**Correct (register migratecmd, import migrations package):**
|
||
|
||
```go
|
||
// main.go
|
||
package main
|
||
|
||
import (
|
||
"log"
|
||
"os"
|
||
|
||
"github.com/pocketbase/pocketbase"
|
||
"github.com/pocketbase/pocketbase/plugins/migratecmd"
|
||
"github.com/pocketbase/pocketbase/tools/osutils"
|
||
|
||
// Import side-effects only; this registers all init() migrations.
|
||
_ "myapp/migrations"
|
||
)
|
||
|
||
func main() {
|
||
app := pocketbase.New()
|
||
|
||
migratecmd.MustRegister(app, app.RootCmd, migratecmd.Config{
|
||
// Automigrate generates a new .go file whenever you make
|
||
// collection changes in the Dashboard (dev-only).
|
||
Automigrate: osutils.IsProbablyGoRun(),
|
||
})
|
||
|
||
if err := app.Start(); err != nil {
|
||
log.Fatal(err)
|
||
}
|
||
}
|
||
```
|
||
|
||
**Create and write a migration:**
|
||
|
||
```bash
|
||
# Create a blank migration file in ./migrations/
|
||
go run . migrate create "add_summary_to_posts"
|
||
```
|
||
|
||
```go
|
||
// migrations/1687801090_add_summary_to_posts.go
|
||
package migrations
|
||
|
||
import (
|
||
"github.com/pocketbase/pocketbase/core"
|
||
m "github.com/pocketbase/pocketbase/migrations"
|
||
)
|
||
|
||
func init() {
|
||
m.Register(func(app core.App) error {
|
||
// app is a transactional App instance – safe to use directly.
|
||
collection, err := app.FindCollectionByNameOrId("posts")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
collection.Fields.Add(&core.TextField{
|
||
Name: "summary",
|
||
Required: false,
|
||
})
|
||
|
||
return app.Save(collection)
|
||
}, func(app core.App) error {
|
||
// Optional rollback
|
||
collection, err := app.FindCollectionByNameOrId("posts")
|
||
if err != nil {
|
||
return err
|
||
}
|
||
collection.Fields.RemoveByName("summary")
|
||
return app.Save(collection)
|
||
})
|
||
}
|
||
```
|
||
|
||
**Snapshot all collections (useful for a fresh repo):**
|
||
|
||
```bash
|
||
# Generates a migration file that recreates your current schema from scratch.
|
||
go run . migrate collections
|
||
```
|
||
|
||
**Clean up dev migration history:**
|
||
|
||
```bash
|
||
# Remove _migrations table entries that have no matching .go file.
|
||
# Run after squashing or deleting intermediate dev migration files.
|
||
go run . migrate history-sync
|
||
```
|
||
|
||
**Apply / roll back manually:**
|
||
|
||
```bash
|
||
go run . migrate up # apply all unapplied migrations
|
||
go run . migrate down 1 # revert the last applied migration
|
||
```
|
||
|
||
**Key details:**
|
||
- Migration functions receive a **transactional** `core.App` – treat it as the database source of truth. Never use the outer `app` variable inside migration callbacks.
|
||
- New unapplied migrations run automatically on every `serve` start – no manual step in production.
|
||
- `Automigrate: osutils.IsProbablyGoRun()` limits auto-generation to `go run` (development) and prevents accidental file creation in production binaries.
|
||
- Prefer the collection API (`app.Save(collection)`) over raw SQL `ALTER TABLE` so PocketBase's internal schema cache stays consistent.
|
||
- Commit all generated `.go` files to version control; do **not** commit `pb_data/`.
|
||
|
||
Reference: [Extend with Go – Migrations](https://pocketbase.io/docs/go-migrations/)
|