Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Plan format

A plan directory is the unit of code review. This guide explains what each file means and how the executor consumes it.

plans/2026-05-11-abc1234567890123/
├── plan.sql        ← canonical artifact (commit this)
├── intent.toml     ← destructive approvals (commit this; flip approvals when ready)
└── manifest.toml   ← plan id, version metadata, embedded pre-image (commit this)

All three files are plain text. Commit them to the same repo as your schema/ tree — the plan directory is part of the migration history.

plan.sql

The applyable artifact. Reads cleanly with psql -f plan.sql for people who want to bypass the executor — pgevolve only relies on the structured -- @pgevolve directive comments to drive transactions and audit logging.

Header directives

-- @pgevolve plan id=abc1234567890123 version=0.3.3 ruleset=1 created=2026-05-11T18:42:11Z
-- @pgevolve source_rev=git:c0ffeeabc
-- @pgevolve target=tid-xyz
-- @pgevolve intents_required=2
DirectiveMeaning
plan id=<16-hex>Short plan id. Must match intent.toml and manifest.toml.
version=<x.y.z>The pgevolve version that produced the plan.
ruleset=<n>Planner ruleset version. Bumps mean the rewrites changed.
created=<rfc3339>UTC timestamp.
source_rev=<rev>Optional source-tree revision (git rev-parse HEAD if you're in a git repo).
target=<id>Stable identifier of the database (hash of host/port/dbname/cluster).
intents_required=<n>How many destructive intents this plan declares.

Group and step directives

Each transactional group is wrapped in BEGIN; ... COMMIT;. Each step has its own directive line:

-- @pgevolve group id=1 transactional=true
BEGIN;
-- @pgevolve step=1 kind=create_table destructive=false targets=app.invoices
CREATE TABLE app.invoices ( ... );
-- @pgevolve step=2 kind=add_constraint_not_valid destructive=false targets=app.invoices.invoices_customer_fk
ALTER TABLE app.invoices ADD CONSTRAINT invoices_customer_fk
  FOREIGN KEY (customer_id) REFERENCES app.customers(id) NOT VALID;
COMMIT;

-- @pgevolve group id=2 transactional=false
-- @pgevolve step=3 kind=create_index_concurrent destructive=false targets=app.invoices.invoices_customer_idx
CREATE INDEX CONCURRENTLY invoices_customer_idx ON app.invoices (customer_id);

-- @pgevolve group id=3 transactional=true
BEGIN;
-- @pgevolve step=4 kind=validate_constraint destructive=false targets=app.invoices.invoices_customer_fk
ALTER TABLE app.invoices VALIDATE CONSTRAINT invoices_customer_fk;
-- @pgevolve step=5 kind=drop_column destructive=true intent_id=1 targets=app.users.legacy_email
ALTER TABLE app.users DROP COLUMN legacy_email;
COMMIT;
Directive fieldMeaning
group id=<n>1-indexed group number.
`transactional=truefalse`
step=<n>1-indexed step number, contiguous across all groups.
kind=<step_kind>Same vocabulary as plan::raw_step::StepKind.
`destructive=truefalse`
intent_id=<n>Present only on destructive steps; references the same id in intent.toml.
targets=<qname1>,<qname2>,...Comma-separated list of affected qualified names.

Step kinds — v0.2 additions (views and materialized views)

The following step kinds were added in v0.2 alongside view and materialized view support:

KindSQL emittedTransactionalNotes
create_viewCREATE VIEW <qname> AS <body>yesUsed for new views and for incompatible body replacements (recreate).
drop_viewDROP VIEW <qname>yesDestructive — requires intent approval.
create_materialized_viewCREATE MATERIALIZED VIEW <qname> AS <body>yes
drop_materialized_viewDROP MATERIALIZED VIEW <qname>yesDestructive — requires intent approval.
refresh_materialized_viewREFRESH MATERIALIZED VIEW [CONCURRENTLY] <qname>no (CONCURRENTLY); yes (without)Upgraded to CONCURRENTLY under online strategy when a unique index is present.
alter_view_set_reloptionALTER VIEW <qname> SET (security_barrier = …)yesAlso handles security_invoker.
comment_on_viewCOMMENT ON VIEW <qname> IS '…'yesUsed for both regular views and materialized views.

Step kinds — v0.2 additions (user-defined types)

The following step kinds were added in v0.2 alongside enum, domain, and composite type support:

KindSQL emittedTransactionalNotes
create_typeCREATE TYPE <qname> AS ENUM (…) / CREATE DOMAIN … / CREATE TYPE <qname> AS (…)yesOne step kind covers all three type families.
drop_typeDROP TYPE <qname>yesDestructive — requires intent approval.
alter_type_add_valueALTER TYPE <qname> ADD VALUE '<label>' [BEFORE|AFTER '<ref>']yes
alter_type_rename_valueALTER TYPE <qname> RENAME VALUE '<from>' TO '<to>'yes
alter_domain_add_constraintALTER DOMAIN <qname> ADD CONSTRAINT <name> CHECK (…)yes
alter_domain_drop_constraintALTER DOMAIN <qname> DROP CONSTRAINT <name>yesDestructive — requires intent approval.
alter_domain_set_defaultALTER DOMAIN <qname> SET DEFAULT <expr> / DROP DEFAULTyesNone default clears the existing default.
alter_domain_set_not_nullALTER DOMAIN <qname> SET NOT NULL / DROP NOT NULLyes
alter_type_add_attributeALTER TYPE <qname> ADD ATTRIBUTE <name> <type>yes
alter_type_drop_attributeALTER TYPE <qname> DROP ATTRIBUTE <name>yesDestructive — requires intent approval.
alter_type_alter_attribute_typeALTER TYPE <qname> ALTER ATTRIBUTE <name> TYPE <new_type>yes
comment_on_typeCOMMENT ON TYPE|DOMAIN <qname> IS '…'yesUses COMMENT ON DOMAIN for domain types; COMMENT ON TYPE for enums and composites.

ReplaceWithCascade. When a type change cannot be applied in place (e.g., dropping or reordering enum values, reordering composite attributes, or changing a domain's base type), the planner emits DROP TYPE <qname> CASCADE followed by CREATE TYPE. Both steps appear in the plan as drop_type (destructive — requires intent approval) and create_type.

Step kinds — v0.2 additions (functions and procedures)

The following step kinds were added in v0.2 alongside function and procedure support:

KindDescription
create_or_replace_functionIdempotent CREATE OR REPLACE FUNCTION. Covers both initial create and in-place body/attribute changes when the language and return type are compatible.
drop_functionDROP FUNCTION <qname>(<arg_signature>). Destructive — requires intent approval.
comment_on_functionCOMMENT ON FUNCTION <qname>(<args>) IS '…'.
create_or_replace_procedureCREATE OR REPLACE PROCEDURE. If the body contains COMMIT or ROLLBACK, the step runs outside a transaction.
drop_procedureDROP PROCEDURE <qname>. Destructive — requires intent approval.
comment_on_procedureCOMMENT ON PROCEDURE <qname> IS '…'.

ReplaceWithCascade for functions. When a function's language or return type changes, CREATE OR REPLACE FUNCTION is not safe. The planner emits DROP FUNCTION <qname> CASCADE (destructive — requires intent approval) followed by create_or_replace_function. The CASCADE propagates to all dependent views and functions automatically.

Step kinds — v0.2 additions (extensions, triggers, partitions)

KindDescription
create_extensionCREATE EXTENSION [IF NOT EXISTS] <name> [WITH SCHEMA <s>] [VERSION '<v>'].
drop_extensionDROP EXTENSION <name> CASCADE. Destructive — requires intent approval.
alter_extension_updateALTER EXTENSION <name> UPDATE TO '<v>'.
comment_on_extensionCOMMENT ON EXTENSION <name> IS '…'.
create_triggerCREATE [CONSTRAINT] TRIGGER <name> … ON <table> ….
drop_triggerDROP TRIGGER <name> ON <table>. Destructive — requires intent approval.
comment_on_triggerCOMMENT ON TRIGGER <name> ON <table> IS '…'.
attach_partitionALTER TABLE <parent> ATTACH PARTITION <child> FOR VALUES ….
detach_partitionALTER TABLE <parent> DETACH PARTITION <child>.

Step kinds — v0.3 additions (cluster roles)

These step kinds appear only in cluster plans (pgevolve cluster plan) — not in per-DB plans.

KindDescription
create_roleCREATE ROLE <name> WITH <options>.
drop_roleDROP ROLE <name>. Destructive — requires intent approval (gating not yet enforced; see v0.3.0 limits in docs/spec/cluster.md).
alter_roleALTER ROLE <name> WITH <options>.
grant_role_membershipGRANT <role> TO <member>.
revoke_role_membershipREVOKE <role> FROM <member>.
comment_on_roleCOMMENT ON ROLE <name> IS '…'.

Step kinds — v0.3.1 additions (ownership and grants)

KindDescription
alter_object_ownerALTER <kind> <qname> OWNER TO <new_owner>.
grant_object_privilegeGRANT <priv> ON <kind> <qname> TO <grantee> [WITH GRANT OPTION].
revoke_object_privilegeREVOKE <priv> ON <kind> <qname> FROM <grantee>.
grant_column_privilegeGRANT <priv> (<col>, …) ON TABLE <qname> TO <grantee> [WITH GRANT OPTION].
revoke_column_privilegeREVOKE <priv> (<col>, …) ON TABLE <qname> FROM <grantee>.
alter_default_privilegesALTER DEFAULT PRIVILEGES FOR ROLE <x> [IN SCHEMA <y>] GRANT/REVOKE <priv> ON … TO/FROM <z>.

Step kinds — v0.3.2 additions (row-level security)

KindDescription
create_policyCREATE POLICY <name> ON <table> ….
drop_policyDROP POLICY <name> ON <table>.
alter_policyALTER POLICY <name> ON <table> TO … USING (…) WITH CHECK (…).
set_table_row_securityALTER TABLE <qname> { ENABLE | DISABLE } ROW LEVEL SECURITY.
set_table_force_row_securityALTER TABLE <qname> { FORCE | NO FORCE } ROW LEVEL SECURITY.

Step kinds — v0.3.3 additions (storage reloptions)

KindDescription
set_table_storageALTER TABLE <qname> SET (fillfactor = …, autovacuum_* = …, …). Batched per object.
set_index_storageALTER INDEX <qname> SET (fillfactor = …, …).
set_materialized_view_storageALTER MATERIALIZED VIEW <qname> SET (fillfactor = …, …).

intent.toml

plan_id = "abc1234567890123"

[[intent]]
id       = 1
step     = 5
kind     = "drop_column"
target   = "app.users.legacy_email"
reason   = "drops column legacy_email"
approved = false

Every destructive step gets one [[intent]] row. intent.toml also supports [[step_override]] rows (see below). The executor:

  • Reads this file at apply time.
  • Refuses to run while any row has approved = false.
  • Records the approval state in the audit log.

Approving destructive intents

Open intent.toml, change approved = false to approved = true for each row you want to allow, and commit the change. The plan id field must stay intact; pgevolve cross-checks it against plan.sql and manifest.toml and rejects mismatches.

Approval is intentional friction. A reviewer should be the one flipping approved = true, not the same person who authored the change. Treat the diff in intent.toml as the "are you really sure?" gate.

[[step_override]] — suppress or skip steps

intent.toml also accepts [[step_override]] rows. Step overrides let you suppress specific planner steps (e.g., skip a refresh_materialized_view during a maintenance window without regenerating the plan):

[[step_override]]
kind = "refresh_materialized_view"
target = "app.daily_summary"
suppress = true
FieldRequiredNotes
kindyesStep kind to match (e.g., refresh_materialized_view, create_view).
targetyesQualified object name the override applies to.
suppressnoDefault false. When true, the matching step is silently omitted from execution. The plan is otherwise applied normally.

When to use. Step overrides are appropriate for one-off operational situations (e.g., deferring an expensive REFRESH to off-peak hours). They are not a substitute for intent approval — destructive steps still require approved = true even when a [[step_override]] is present.

manifest.toml

plan_id                 = "abc1234567890123"
plan_hash               = "abc1234567890123…"  # full 64-char hex
pgevolve_version        = "0.3.3"
planner_ruleset_version = 1
source_rev              = "git:c0ffeeabc"
target_identity         = "tid-xyz"
created_at              = "2026-05-11T18:42:11Z"
target_snapshot_json    = "..."                # embedded pre-image catalog as pretty-printed JSON
FieldMeaning
plan_idShort plan id; matches plan.sql and intent.toml.
plan_hashFull 64-char BLAKE3 hex. Recomputable from (source, target, version, ruleset).
pgevolve_versionThe pgevolve build that wrote the plan.
planner_ruleset_versionPlanner ruleset version.
source_revOptional source-tree revision.
target_identityTarget-DB identity hash.
created_atUTC timestamp.
target_snapshot_jsonEmbedded pre-image Catalog as JSON. Used at apply time for drift detection — pgevolve compares this against the live state to make sure no out-of-band changes happened since planning.

Round-trip property

write_plan_dir(&p, dir); read_plan_dir(dir) == p — modulo the grafted destructive_reason (which lives in intent.toml, not in plan.sql). The round-trip is property-tested over random catalogs.

What pgevolve does and does NOT touch

Filepgevolve writespgevolve reads at apply
plan.sqlyes (planner)yes (executor)
intent.tomlyes (planner) with approved=falseyes (executor reads the flipped values)
manifest.tomlyes (planner)yes (executor cross-checks plan id + reads pre-image for drift)

pgevolve never modifies a plan directory after it's written. Edits to intent.toml's approval flags are made by users (typically in a code review). Anything else is suspicious.