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.”