release

Bootstrapping Now Runs on MySQL and MariaDB and Posts Seed Plans on Pull Requests

Bootstrapping 0.3 is out. It now runs on MySQL 8.0 and later and MariaDB 10.6 and later, as well as SQLite and PostgreSQL. A reference can name its row by several columns. And the repository is now a GitHub Action that posts the seed plan on a pull request and fails the check when a conflict would block the apply.

Most Rails, Laravel and Django applications with reference data run on MySQL or MariaDB, so they were the next databases to support. The seed files don’t change. Give -db a URL and the same plan, apply, explain and history work there:

bst apply -db 'mysql://app:[email protected]:3306/app?tls=true' -seeds seeds

Bootstrapping 0.3: one seed folder applied to SQLite, PostgreSQL, MySQL and MariaDB, a grant naming its role by region and code, and a pull request comment with the seed plan and a failed check for a conflict

What MySQL Needed

MySQL’s defaults are loose. Outside strict mode, a value too long for its column is cut short with a warning, and a date that doesn’t exist becomes zeros. A tool whose whole job is to keep values exactly as intended can’t work that way. So 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. Seed values go through the server too: each is written into a temporary table whose column has the real column’s type, character set and collation, then read back. So 7 in a DECIMAL(10,2) column is 7.00, a SET value comes back in its own order, and a value that doesn’t fit stops the plan with the file, row and column named.

Locking took more care. On PostgreSQL, an apply that finds a row changed under it fails and writes nothing. InnoDB doesn’t do that at REPEATABLE READ: it lets the later write through. The tests caught it, with an admin editing a row while an apply ran. Now an apply reads the managed tables with shared locks, so an admin’s edit either lands before the apply reads the row, or waits until the apply has finished. Two applies at once take turns through a named lock, as before.

Two things are refused outright. A MyISAM or MEMORY table can’t roll back, so an apply that failed halfway would leave part of a seed behind; Bootstrapping won’t manage one. And keys are compared as exact text. MySQL’s default collation ignores case, so a key column using it can’t hold two seed keys that differ only in case. The reference says what to do about both.

References by Several Columns

Until now a link between tables named its row by one column: role_id = "roles.code". Join tables are often keyed on two, a region and a code, or a tenant and a name. Now the reference can say so:

[refs]
role_id = "roles.(region, code)"

[[row]]
code = "g1"
role_id = ["eu", "editor"]

In a CSV file the cell is eu|editor. The database still stores the role’s id. Everything a one-column reference does works the same: a renamed role keeps its id and the link follows it, a role that something still points at isn’t deleted, and a table can point at its own rows.

The Plan on Every Pull Request

0.2 could print the plan as Markdown, but posting it was left to each team’s CI. Now one step does it:

- uses: bootstrapping-engine/[email protected]
  with:
    database: ci.db
    seeds: seeds

The action downloads the release for the runner, checks it against the release’s checksums, plans, and posts the result as a comment. Each later push edits that same comment instead of adding another. When the plan has a conflict nobody settled, the check fails, so a seed change that would stop the next deploy can’t be merged by accident.

The independent review of this release found that a database password could reach a public comment. Some driver errors quote the connection string, and the command printed them as they came. Now every error is scrubbed of the URL and its password, in its plain, quoted and escaped forms, and the action removes it again and masks it in the log. A test checks each form.

Tested on Three Servers

The tests that guard the merge now run on MySQL too. On MySQL 8.4 the random histories came out identical to SQLite and PostgreSQL, figure for figure: 492,228 values checked against the merge rules with none overwritten without a conflict, 47,330 local edits kept, and 6,000 applies through linked tables with no key broken. 100 applies killed with SIGKILL halfway through left the database exactly as it was (68) or exactly as a finished apply leaves it (32). In 60 rounds of eight processes applying at once, exactly one wrote each time. CI runs it all on every push against MySQL 8.0 and 8.4 and MariaDB 10.6 and 11.8.

The commands now use a pure Go SQLite driver, so the binaries and a build from source need no C compiler, and the library needs Go 1.26. The download page has the binaries, the reference has the MySQL details, and the quick start shows the MySQL version of the walk-through.