Quick Start
This page runs the demo’s story by hand: an application’s roles and permissions, three versions of its seed files, and an admin who changes things in between. Every output below is what the command really prints.
Get bst from the download page, and the example from the repository:
git clone https://github.com/bootstrapping-engine/bootstrapping
cd bootstrapping/examples/permissions
sqlite3 app.db < schema.sql
export BOOTSTRAPPING_DB=app.db
The schema makes three tables: roles, permissions, and the links between them. Bootstrapping fills tables but never creates them; that stays with your migrations.
The First Version
$ bst status -seeds seeds/v1
Pending: 18 inserts.
$ bst apply -seeds seeds/v1 | tail -1
Applied as run 1: 18 inserts.
status exits with 2 when an apply would write something, so a deploy script can tell. apply writes everything in one transaction, together with a ledger of what it applied.
An Admin Changes Things
A customer’s admin renames the Editor role and takes the delete permission away from editors:
sqlite3 app.db "UPDATE roles SET name = 'Content Editor' WHERE code = 'editor'"
sqlite3 app.db "DELETE FROM role_permissions
WHERE role_id = (SELECT id FROM roles WHERE code = 'editor')
AND permission_id = (SELECT id FROM permissions WHERE code = 'posts.delete')"
The Second Version
The next release adds a billing permission, renames Viewer to Reader and rewrites the Editor’s description:
$ bst plan -seeds seeds/v2
permissions
insert billing.view
roles
update editor description "Writes and publishes posts" -> "Writes, edits and publishes posts"
keep edit editor name local "Content Editor"; the seed still says "Editor"
update viewer name "Viewer" -> "Reader"
role_permissions
insert admin, billing.view
keep delete editor, posts.delete deleted locally; it stays deleted
Pending: 2 inserts, 2 updates, 1 local edit kept, 1 local delete kept.
$ bst apply -seeds seeds/v2 | tail -1
Applied as run 2: 2 inserts, 2 updates, 1 local edit kept, 1 local delete kept.
The admin’s name stays, because the seed didn’t change it. The description changes, because the admin didn’t touch it. The deleted link stays deleted. Nothing had to be written by hand to get there.
A Conflict
The third version renames Editor in the seed too, to “Editorial”, and drops the users.manage permission:
$ bst apply -seeds seeds/v3
permissions
retire users.manage dropped from the seed
roles
CONFLICT editor name seed "Editor" -> "Editorial", local "Content Editor"
role_permissions
keep delete editor, posts.delete deleted locally; it stays deleted
delete admin, users.manage dropped from the seed
Blocked: 1 delete, 1 retire, 1 conflict, 1 local delete kept.
Nothing was written. Settle the conflicts with -take, such as -take local or -take roles/editor/name=local, or with on_conflict in the seed file.
Both sides changed the same value, differently, so the apply stops and exits with 3. Nothing is written, not even the parts that didn’t conflict. Keep the admin’s name:
$ bst apply -seeds seeds/v3 -take roles/editor/name=local
permissions
retire users.manage dropped from the seed
roles
CONFLICT editor name seed "Editor" -> "Editorial", local "Content Editor" (settled: the local value)
role_permissions
keep delete editor, posts.delete deleted locally; it stays deleted
delete admin, users.manage dropped from the seed
Applied as run 3: 1 delete, 1 retire, 1 settled conflict, 1 local delete kept.
$ bst status -seeds seeds/v3
In step with the seed (1 local edit kept, 1 local delete kept).
The conflict doesn’t come back. The ledger now records “Editorial” as applied, so “Content Editor” counts as a local edit from here on. To decide once for good instead, put it in the seed file: name = { own = "merge", on_conflict = "local" }.
Looking at One Row
explain shows a row value by value: as last applied, as the seed has it, as the table has it, and the verdict, then its history.
bst explain -seeds seeds/v3 roles editor
Every command takes -json, and bst history lists the applies the ledger recorded. The reference has every command and setting.
From Go
Embed the seeds in your binary and apply them at startup, with whichever SQLite driver your application already uses:
import (
"embed"
"github.com/bootstrapping-engine/bootstrapping"
)
//go:embed seeds
var seeds embed.FS
func applySeeds(ctx context.Context, db *sql.DB) error {
set, err := bootstrapping.LoadFS(seeds, "seeds")
if err != nil {
return err
}
plan, err := bootstrapping.Apply(ctx, db, set, nil)
if errors.Is(err, bootstrapping.ErrBlocked) {
for _, c := range plan.Changes {
if c.Blocking() {
log.Printf("conflict: %s %s %s", c.Table, c.KeyText(), c.Column)
}
}
}
return err
}
The Demo
bootstrapping-demo runs this same story with its own checks, and also kills an apply with SIGKILL after its first statement to show the database left exactly as it was. It ends with “All checks passed.”