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
group_id = "groups.(region, code)" # or name the row by several columns
[[row]]
code = "editor"
name = "Editor"
system = true
parent_id = "admin"
_renamed_from = "author" # keeps the row's id when its key changes
A table’s rows can come from a CSV file instead of [[row]] tables: rows = "countries.csv", with the column names on the first line; an empty cell is NULL.
A reference by several columns takes a list in the seed, group_id = ["eu", "staff"], and in a CSV cell the values joined by |, as in eu|staff. Renames, deletes held while something points at a row, and references within one table work the same as with one column.
Columns a row leaves out are NULL. Every other column is merge. Values are compared the way the database stores them in that column. On SQLite 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 or URL] [-seeds DIR] [-json]
-db takes a SQLite file or a postgres://, mysql:// or mariadb:// URL, and 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] [-markdown] |
lists what an apply would do; -markdown for a pull request comment |
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, PostgreSQL, MySQL or MariaDB 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 (on PostgreSQL, in the first schema of the search path; on MySQL and MariaDB, in the connection’s 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).
On MySQL and MariaDB
Each plan or apply runs on one connection with a strict sql_mode and the time zone set to UTC, and puts both back afterwards. Two applies at once take turns through a named lock. During an apply the managed tables are read with shared locks, so an admin’s edit either lands before the apply reads the row or waits until it has finished; it is never overwritten. Seed values are converted by the server through a temporary table with the column’s own type, so a value that doesn’t fit stops the plan with the row and column named. Tables must use a transactional engine such as InnoDB; a MyISAM or MEMORY table is refused, since it couldn’t be rolled back.
In a Pull Request
The repository is also a GitHub Action. It plans the seed folder against a database, posts the plan as one comment that later pushes update, and fails the check when a conflict would block the apply:
- uses: bootstrapping-engine/[email protected]
with:
database: ci.db
seeds: seeds
The database URL never appears in the comment or the log. docs/ACTION.md has every input and a complete workflow.
Limits in Version 0.3
- SQLite, PostgreSQL 12 or later, MySQL 8.0 or later and MariaDB 10.6 or later. On PostgreSQL, tables are found through the connection’s
search_pathand the apply needs a direct connection or a pooler in session mode. On the three servers, a table a reference points at needs a one-column primary key. - On MySQL and MariaDB, keys are compared as exact text, so a key column whose collation ignores case can’t hold two seed keys that differ only in case.
- Built for reference data: thousands of rows per table, not millions.