Reference

A short reference for the seed files, the command and the Go library. The full versions are in the repository: the seed format, the merge rules, and the command, the API and the ledger.

Seed Files

A directory of .toml files, one per table:

table = "roles"
key = "code"                           # a column, or a list for a composite key
on_remove = { retire = "retired_at" }  # "keep" (default), "delete", or { retire = "column" }
on_conflict = "fail"                   # "fail" (default), "seed" or "local"
on_local_delete = "respect"            # "respect" (default) or "restore"

[columns]
system = "seed"                        # the seed always wins
notes = "local"                        # set on insert, then the admin's
name = { own = "merge", on_conflict = "local" }

[refs]
parent_id = "roles.code"               # store the id of the roles row with that code

[[row]]
code = "editor"
name = "Editor"
system = true
parent_id = "admin"
_renamed_from = "author"               # keeps the row's id when its key changes

Columns a row leaves out are NULL. Every other column is merge. Values are compared the way SQLite stores them in that column, so a seed’s 1 in a TEXT column matches the text “1”.

The Merge

For every value, with the new seed, the table and the last-applied seed from the ledger:

Situation Result
table matches the seed nothing to do
local column the table’s value stays
seed column the seed’s value is written
changed in the seed only the seed’s value is written
changed in the table only the table’s value stays
changed on both sides, differently conflict
row was already there, never applied, different value conflict

For rows: new seed rows are inserted, rows deleted locally stay deleted (or come back with on_local_delete = "restore"), rows dropped from the seed follow on_remove, and a dropped row that was edited locally is a conflict before it’s deleted. A row due for deletion that something still points at is skipped until nothing does, and a row whose reference names a row that doesn’t exist is skipped and tried again next time. Rows the seed never named are never touched.

Commands

bst <command> [-db FILE] [-seeds DIR] [-json]

-db defaults to $BOOTSTRAPPING_DB, -seeds to $BOOTSTRAPPING_SEEDS or seeds. bootstrapping and bst are the same command. Flags can go before or after a command’s other arguments.

Command Does Exit codes
plan [-v] lists what an apply would do 0 in step, 2 pending, 3 blocked
apply [-take ...] does it, in one transaction 0 done, 3 blocked with nothing written
status one summary line as plan
explain TABLE KEY one row, value by value, with its history 0
history [-n N] the applies, newest first 0
init creates the ledger tables 0
version the version 0

Any error exits with 1. -take settles conflicts and can be repeated: seed or local for every conflict, or TABLE, TABLE/KEY or TABLE/KEY/COLUMN followed by =seed or =local.

Go

set, err := bootstrapping.LoadDir("seeds")                 // or LoadFS(embedFS, "seeds")
plan, err := bootstrapping.Preview(ctx, db, set, nil)      // plans, writes nothing
plan, err = bootstrapping.Apply(ctx, db, set, &bootstrapping.Options{
	Take: []bootstrapping.Resolution{{Table: "roles", Column: "name", Take: bootstrapping.TakeLocal}},
})                                                         // ErrBlocked if a conflict stops it
rep, err := bootstrapping.Explain(ctx, db, set, "roles", "editor", nil)
runs, err := bootstrapping.History(ctx, db, 10)

db is a *sql.DB from any SQLite driver. A Plan lists its Changes, each with the table, the key, the action, the column, and the last-applied, seed and live values.

The Ledger

Four tables in your database, created on the first apply and written in the same transaction as the data: _bootstrapping_rows (the values last applied, per row), _bootstrapping_runs (one row per apply), _bootstrapping_changes (every write, for explain and history) and _bootstrapping_refs (the references last applied, so a column the seed stops naming is still read by natural key).

Limits in Version 0.1

  • SQLite only. PostgreSQL is planned next.
  • A reference names a row by one column.
  • Built for reference data: thousands of rows per table, not millions.