besu generate introspects the database, diffs it against a schema snapshot, and writes the statements into a numbered SQL file. It is the step between changing a struct and applying a reviewed migration.

Usage: besu generate [OPTIONS] --database <DATABASE> --schema <SCHEMA>

Options:
      --database <DATABASE>  The database to diff against, for example `postgres://user:pass@host/db` [env: DATABASE_URL=]
      --schema <SCHEMA>      The schema snapshot that describes the desired state
      --out <OUT>            The directory to write migration files into [default: migrations]
      --name <NAME>          The name to give the generated migration [default: schema]
  -h, --help                 Print help

--database also reads DATABASE_URL. Both --database and --schema are required; the defaults above apply to --out and --name.

Producing the snapshot

The snapshot is the desired state, so it comes from your code rather than from the database:

let json = besu_sqlite::Database::schema_snapshot([users.def(), posts.def()])?;
std::fs::write("schema.json", json)?;

Schema::from_defs(..) with Schema::save is the same thing when you already have a Schema value. The file is the IR from the overview.

Running it

besu generate --database sqlite:app.db --schema schema.json --out migrations
# wrote 3 statement(s) to migrations/0001_schema.sql
besu generate --database postgres://user:pass@host/db \
    --schema schema.json --out migrations --name add_email_index
# wrote 1 statement(s) to migrations/0002_add_email_index.sql

If the database already matches the snapshot, nothing is written:

schema is up to date; nothing to generate

That message is not an error, and the exit status is zero.

What it writes

The filename is {version:04}_{name}.sql. The version is one more than the highest numeric prefix already in the directory, scanning every entry, so 0007_... and 7_... both count as seven. The name is --name with spaces turned into underscores and lowercased; other punctuation is left alone.

Every file starts with the same two lines:

-- Generated by `besu generate`.
-- Review before applying: dropping a column loses its data.

A -- warning is added when the dialect reports that it cannot change a column in place and the plan contains an AlterColumn. None of the three shipped dialects takes that path: SQLite rebuilds the table and reports that it can, so its generated files carry the standard header and nothing more.

Each rendered statement follows on its own line, terminated with ;.

What it does not do

  • It does not write a down migration. generate has no way to invent the inverse of a dropped column, so the .down.sql is yours.
  • It does not update the journal. Generating a file does not mark it applied.
  • It does not filter by safety. A DropColumn or DropTable is written like anything else.
  • It does not read the migration files. The diff is always snapshot against live database; the directory is scanned only for the next version number.

Gotchas

  • Run it against a database at the current migration version. If earlier migrations have not been applied, the generated file repeats their changes under a new version.
  • A snapshot from besu pull contains the database. Diffing a pulled snapshot against the same database produces an empty file, which is a useful check but not a migration.
  • Generating twice without applying writes two files with the same content. The second version number is higher; neither knows about the other.
  • The name is not sanitised beyond spaces and case. --name "add-index" produces 0002_add-index.sql.
  • A destructive statement in the file still runs. besu migrate does not refuse it; only push has a safety check.
  • A SQLite AlterColumn is a table rebuild. The file creates a new table, copies the rows, drops the old one and renames; a trigger is not recreated. The header does not warn, because SQLite reports that it can alter the column.