`besu push`
besu push applies a schema snapshot directly, with no migration file and no history. It
introspects the database, diffs it against the snapshot, and runs the difference, refusing
anything that would lose data unless --destructive is given.
--database also reads DATABASE_URL.
Running it
Every statement is printed after the run, followed by the summary. An up-to-date schema
prints schema is up to date and exits zero.
The library equivalent is Database::push_schema, or Database::push for table
definitions:
--dry-run
--dry-run connects, introspects and diffs, then prints one line per change instead of
running anything. Each line is prefixed with its safety:
It is a real preview of the plan push would apply, using the same normalise_type folding,
so a type the dialect cannot distinguish does not appear. It prints change descriptions with
their safety, not SQL — besu generate is what writes statements.
Safety
push classifies every change and refuses the whole plan when any of them is Destructive:
Risky changes run. They can be slow, or fail on existing rows, but they do not silently throw data away. A refusal lists every destructive change and stops before the first statement:
The CLI's equivalent is --destructive; the library calls it push_destructive or
push_schema_destructive.
What it ignores
push manages tables, columns, indexes, uniques, foreign keys, checks, PostgreSQL enum types
and — when the declaration names them — views and sequences. Everything else the IR cannot
express is left exactly as it is:
- row-level security policies, see row-level security;
- extensions, see extensions;
- triggers, expression indexes,
INCLUDEindexes and exclusion constraints.
Views and sequences are opt-in, and this is the one asymmetry worth memorising. A
declaration that names no views cannot distinguish "I did not mention views" from "I want
them all gone", and the first is the common case — push(&[TableDef]) cannot express a view
at all. So an empty list means "views are not mine", and an existing view is left alone. Name
at least one view (or sequence) in the snapshot and the whole list is managed, including
drops. This is the same shape as enums, which are only dropped when nothing declares them.
Without that rule, adding a view as a migration would make every later push(&[TableDef])
refuse the plan as destructive — which is exactly what happened before it was added.
Gotchas
pushis not transactional. Statements run one at a time, so a failure halfway leaves the earlier ones applied.- There is no journal.
pushdoes not record what it did, sobesu migrateafterwards has no idea the schema already moved. - A rename is a drop plus an add unless declared. Add
#[besu(renamed_from = "old")]and the plan becomes oneALTER TABLE ... RENAME COLUMN; without it,pushrefuses. - SQLite rebuilds a table to change a column. Rows are copied across, but a trigger is not, because the IR does not model one.
- Type widening can be invisible. SQLite folds integer widths; PostgreSQL folds unsigned integers into their signed storage. A change the dialect cannot see is not reported.
- A check written by hand is not declared by the derive. Pushing table definitions
against a table with a check reports a
DropCheck, which is destructive, sopushrefuses.
