Skip to content

Releases: jokull/onwardpg

v0.1.0-preview.15

v0.1.0-preview.15 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 19 Oct 09:39
552ce4d

Fixed

  • An expression whose catalog nests an AND or OR chain no longer reads as
    changed against a catalog that holds the same chain flat. BETWEEN keeps
    its own AND node, so CHECK (x BETWEEN 1 AND 5 AND y > 0) deparses as
    (((x >= 1) AND (x <= 5)) AND (y > 0)). A dump of that catalog restores
    the text flat, and drift check and contract check against the restored
    copy reported the constraint as changed_in_actual. Inspection now records
    table CHECK constraints, partial index predicates, index expressions,
    column defaults, and generated columns in the flat form, which is what
    PostgreSQL deparses for the flat expression, so the two catalogs compare and
    fingerprint equal. Domains, policies, views, and triggers were already read
    flat. A schema with such an expression has a new fingerprint, so a pending
    bundle planned over it before this release no longer matches its own
    receipts: run onwardpg plan and onwardpg verify for it again.
  • answers.json and decisions.json list answers by question id (kind and
    key). Before, they kept the order in which the answers were given, so a
    bundle planned with an incremental hint loop had a different entry digest
    than the same bundle planned once with the final hint set, and a second
    plan rewrote it. One plan of a given input now gives one entry digest. A
    bundle written by an earlier release keeps its digests until it is planned
    again.

v0.1.0-preview.14

v0.1.0-preview.14 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 18 Oct 07:28
4c14817

Fixed

  • An ignored object now masks the unsupported state of itself and of the
    objects in it, in every command that takes ignore selectors. Before,
    contract check --ignore schema:legacy still answered unsupported with
    acl:schema:legacy when the schema was granted to pg_read_all_data:
    the selector removed the schema and its objects, but not their ACLs.
    schema:NAME now masks the schema's ACL, ownership, and IN SCHEMA
    default ACLs, and the ACL, ownership, storage, and comment state of each
    table, column, index, sequence, type, and routine in it. table:,
    column:, index:, constraint:, sequence:, routine:, type, and
    extension: selectors mask the state of their own object, and table:
    also that of its columns and indexes. Unsupported state of a kept object,
    and cluster or database state such as a default ACL for every schema or a
    parameter grant, still blocks. A masked marker has no ignore receipt of its
    own, so a catalog that a checkpoint accepted keeps its fingerprint.
    contract check lists the masked markers after the masked objects in
    ignored.expected_masked and ignored.actual_masked.
  • A new NOT NULL column without a default no longer promotes its dependents
    to contract. Expand adds the column as nullable and contract sets it NOT
    NULL. That contract work made the column a contract provider, so each
    UNIQUE constraint, foreign key, CHECK, index, and view over it moved to
    contract with dependency_phase_promotion. A promoted UNIQUE or foreign key
    has no gate, so the plan became unsupported with
    contract_enforcement_missing_gate. A new quote.company NOT NULL in two
    UNIQUE constraints and in a composite foreign key from payment_due did
    this. preview.12 exempted a column modification of this kind; a new column
    now follows the same rule. A new primary key over the column is contract
    work of its own and still follows SET NOT NULL.
  • A sequence, type, or routine ACL that holds exactly the default privileges,
    as GRANT then REVOKE leaves it, is no longer unsupported state. It means
    what the NULL ACL means. Schema and table ACLs already compared this way.
    The items are compared as sets, and an object with pg_init_privs, a
    former extension member, keeps its explicit ACL.

v0.1.0-preview.13

v0.1.0-preview.13 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 18 Oct 04:56
3154e8e

Added

  • onwardpg contract check --ignore SELECTOR takes the selectors of
    drift check --ignore and applies them to both sides of the checkpoint
    comparison. A cutover database can hold objects that are not drift for the
    bundle: an archive schema that the operator created in the window, a column
    of another pending change, or foreign keys that a restore could not create.
    Before, the check answered catalog_drift and could not become ready. The
    checkpoint is a fingerprint, so with --ignore the command replays the
    history through the bundle's expand in the target's scratch database, as
    verify does when it receipts the checkpoint, proves that the replay
    reproduces the receipted fingerprint (expand_checkpoint_not_reproduced
    otherwise), and compares both sides with the selectors applied. A selector
    that matches nothing is invalid_ignore. The report adds ignored: each
    selector with the receipts it matched on each side, every masked object on
    each side, and the compared fingerprints; a ready report keeps it. When the
    catalogs still differ, differences lists the objects as drift check
    does. Without --ignore the command is unchanged.

Changed

  • schema:NAME now ignores the schema and every object in it, in every
    command that takes ignore selectors. Before, it removed only the schema
    node, so the tables of an ignored schema were still compared. A foreign key
    from a kept table into the ignored schema is an error, as one into an
    ignored table already was, so an ignored object cannot hide drift in a kept
    one. Unsupported-state markers in the schema are not acknowledged by it.
  • When two given selectors match the same object, such as schema:NAME and
    a selector of an object in that schema, or table:* and one table, both
    are used. Before, only the first of them was marked used, and the other was
    reported as an unused selector.
  • drift check and contract check report an ignore-selector error from the
    live inspection or the scratch replay, such as a foreign key into an
    ignored table, as invalid_ignore.

v0.1.0-preview.12

v0.1.0-preview.12 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 18 Oct 02:39
5f47655

Fixed

  • A column whose only contract work is ALTER COLUMN ... DROP DEFAULT or
    SET NOT NULL no longer promotes its dependents to contract. Before, every
    change with a contract statement promoted each desired dependent with the
    dependency_phase_promotion hazard. One DROP DEFAULT on quote.status
    moved its CHECK change, an index, and 11 views into contract, so the
    window between expand and contract ran without them. A promoted CHECK
    change could also lose its validation gate and make the plan unsupported.
    No CHECK, index, or view reads a column's default or nullability, so these
    dependents now keep their own phase. A new primary key or
    REPLICA IDENTITY USING INDEX still follows SET NOT NULL: both are
    contract work of their own. A type, collation, identity, serial, or
    generated-expression change still promotes its dependents, and so does a
    domain default change, because ADD COLUMN over the domain fills existing
    rows with the domain default of that moment. A column that is itself
    promoted still promotes its dependents.

v0.1.0-preview.11

v0.1.0-preview.11 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 17 Oct 20:45
bcc8016

Added

  • onwardpg contract check --bundle ID can check a bundle that is not the
    history head, for a cutover window that runs several bundles back to back
    (expand 1, contract 1, expand 2, ...) behind a write barrier. The check is
    ready only while the database matches that bundle's receipted post-expand
    catalog, which exists only after every earlier contract and before any
    later expand. Out-of-order states are refused with
    predecessor_contract_pending, expand_not_applied,
    contract_already_applied, or successor_expand_applied. The report adds
    history_head, chain_position, and chain_length.

  • --evidence accepts a JSON array of writer-evidence objects, one per
    bundle, so one file attests the write barrier for the whole pending chain
    prefix. The check uses the object bound to the selected bundle and refuses
    a file with none or with two. A ready report records the accepted
    attestation under evidence: digest, release, observation window, and
    cohorts. Build the file from the bundle manifests and check each contract
    in chain order:

    jq -n \
      --arg environment production --arg release cutover-2026-10-18 \
      --arg observed_at 2026-10-18T06:00:00Z --arg expires_at 2026-10-18T10:00:00Z \
      --slurpfile cohorts write-barrier-cohorts.json \
      '[inputs | {target, environment: $environment, plan_id,
        bundle_entry_digest: .history.entry_digest,
        desired_fingerprint: .desired_source.fingerprint, generation,
        release: $release, observed_at: $observed_at, expires_at: $expires_at,
        cohorts: $cohorts[0]}]' \
      migrations/onward/app/T1/manifest.json \
      migrations/onward/app/T2/manifest.json > cutover-evidence.json
    
    onwardpg contract check --target app --bundle T1 \
      --environment production --database-env PROD_READONLY_DATABASE_URL \
      --evidence cutover-evidence.json

    write-barrier-cohorts.json lists one cohort per required category (web,
    workers, scheduled_jobs, queues, connection_pools, previews,
    ad_hoc_writers), each with status read_only, drained, or isolated,
    source_kind manual, and a source that names the barrier. The
    observation window must not exceed 24 hours.

Fixed

  • A table or column rename on PostgreSQL 18 no longer names the replay's NOT
    NULL constraint. PostgreSQL appends a collision suffix (not_null1,
    not_null3, ...) to a generated name past 63 bytes, and the suffix depends
    on creation history, so production and onwardpg's replay can name the same
    constraint differently. Before, the plan emitted ALTER TABLE ... RENAME CONSTRAINT <replay name> TO ..., which failed on production. The plan now
    finds the constraint by table and column in pg_constraint at apply time
    and renames it only when its name differs from the target.
  • Planning the same inputs twice writes a byte-identical bundle. Before, the
    rename_table rejection reason named whichever mismatching child Go map
    iteration reached first, so plan.json and the plan, result, and history
    entry digests changed between runs while the phases stayed the same. The
    reason now names the first mismatch in stable key order. The same fix
    covers the order of replaced and validated domain-constraint statements,
    the enum_rewrite_column_change and derived_view_closure_object
    reasons, and which contract gate names a draft edit requirement.

v0.1.0-preview.10

v0.1.0-preview.10 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 17 Oct 17:01

Fixed

  • A table rename keeps the foreign keys that point at it. A catalog export
    prints each such foreign key with the new REFERENCES target and the
    referenced key index under its derived new name (users_pkey ->
    accounts_pkey). Before, every inbound foreign key became a drop, ADD ... NOT VALID, and VALIDATE, and a self-referencing foreign key made the
    table lose its rename candidacy (child_semantics_changed). Any other
    foreign-key change is still planned.
  • A column rename keeps the foreign keys on either end. The owning table's
    renames rewrite the local column list and the ON DELETE SET NULL/DEFAULT
    list; the referenced table's renames rewrite only the REFERENCES column
    list. Before, each such foreign key became a reconcile question and a drop
    and re-add.
  • A CHECK, index, or foreign key that names several renamed columns is judged
    against every rename of the plan in one pass. Before, it never matched and
    was rebuilt (a CHECK through NOT VALID and VALIDATE).
  • A column rename carries an index that names the column in its WHERE
    predicate, a key expression, or INCLUDE. Before, such an index was dropped
    in expand and rebuilt in contract, so a partial UNIQUE index enforced
    nothing between the two phases. Predicate and expression columns are
    matched by whole identifier tokens; a token in a function or type position
    is never rewritten, so lower(lower) -> upper(upper) is still an index
    change.
  • A table rename ignores declared column order. A column that a later
    migration appended but the schema declares mid-table no longer costs the
    table its rename candidacy (child_semantics_changed:column:...).

v0.1.0-preview.9

v0.1.0-preview.9 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 16 Oct 16:54
176a611

Added

  • rename_backfill_strategy offers direct_rename: one native
    ALTER TABLE ... RENAME COLUMN in expand, with no overlap bridge. The
    statement carries the single_deployment_cutover hazard, because the
    previous release stops working the moment it runs. When existing triggers,
    defaults, generated values, partitions, or row security withhold the
    generated bridge, the question offers only direct_rename and split_plan
    and names the reason. Before, such a table made the whole plan
    unsupported as soon as one rename was confirmed. direct_rename renders
    the name change and nothing else, so it is withheld when more than the
    name differs or a dependent view changes, and a partition child's column
    is renamed by its parent's statement.

  • A new UNIQUE constraint on an existing table whose columns contain the
    columns of a key that PostgreSQL already enforces there (the primary key,
    or a unique key with the same NULL treatment) is catalog-proven: no
    reconcile_contract question, expand phase, hazard
    unique_implied_by_existing_key:<key>. A foreign key from a new table that
    references it stays in expand. Before, the key took the contract path and
    that foreign key stopped the plan with contract_enforcement_missing_gate.

  • When the generated plan does not converge after contract, the checkpoint
    error names the objects whose definition or dependencies differ from the
    desired schema, instead of two fingerprints.

Fixed

  • A column rename answer on a table that cannot take the overlap bridge no
    longer stops the planner before the other rename answers are read. With
    several renames on one table, plan --hints-file failed with
    unused answers for every rename after the first, and a single --hint
    stopped as unsupported; no sequence of answers could complete the plan.
  • The workflow report lists every planner blocker as a finding (code = the
    blocker family, for example single_deployment_column_bridge_required).
    Before, only column_physical_order was shown, so an unsupported plan
    could show nothing but informational analysis.
  • cross_type_column_identity analysis no longer lists a drop/create pair
    once either column has a confirmed rename. A table with ten renames and
    seventeen new columns reported 92 pairs, including pairs for columns the
    hints had already decided.
  • Dependents of a direct_rename column (indexes, keys) are no longer
    promoted to contract: the desired name exists after expand.
  • Resolving a confirmed rename of an indexed column rewrote the index parts
    of the current snapshot in place. Every later build in the same process
    then computed a different current fingerprint and rejected the receipted
    answers as stale.

v0.1.0-preview.8

v0.1.0-preview.8 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 07 Oct 19:02
2a30653

Fixed

  • drift check, plan, and draft work with a history that holds
    non-transactional batches. A bundle from plan --concurrent-indexes has
    CREATE INDEX CONCURRENTLY or DROP INDEX CONCURRENTLY in a
    non-transactional batch. verify ran such a bundle correctly, but
    drift check, and the base history replay of plan and draft, sent the
    complete history to PostgreSQL as one query. PostgreSQL runs such a query in
    one implicit transaction, and the command stopped with CREATE INDEX CONCURRENTLY cannot run inside a transaction block (SQLSTATE 25001). After
    such a bundle was accepted, no drift check and no new plan was possible.
    There is now one history replay, the one that verify uses. It runs each
    batch in the mode that the bundle declares, splits an edited phase at its
    batch directives, runs the checks that verification runs, and uses one
    connection for the complete history, so session state such as search_path
    from a baseline stays set. Bundle formats and digests are unchanged, and no
    bundle has to be planned again.
  • A bundle with more than one statement that cannot run in a transaction
    block stays verifiable after its phase file is edited. The generator wrote
    one -- onwardpg:batch nontransactional line for a batch that could hold
    several such statements, for example two CREATE INDEX CONCURRENTLY from
    concurrent_indexes = true. An edited phase is split only at those lines
    and each chunk is one query, so after any edit of the file, even a comment,
    verify failed with non_transactional_batch_failed and SQLSTATE 25001.
    The planner now makes each such statement a batch of its own, so the phase
    file has one directive for each. verify, drift check, and the base
    replay of plan and draft then run an edited phase statement by
    statement.
  • drift check reads the live catalog before it replays history. A role that
    the observer guard refuses, or a wrong URL, now stops the command in about a
    second. Before, each such attempt cost a full history replay first.
  • drift check no longer requires access to application objects. It reads
    system catalogs only, which every PostgreSQL role can read, and a role
    without USAGE or SELECT on anything reads the same graph as the database
    owner (proved by test on PostgreSQL 15 to 18). The check that stopped the
    command with drift_observer_access_incomplete, and that listed objects
    that --ignore excluded, such as the relations of a provider schema, is
    removed from drift check and dev plan. contract check keeps it,
    because its data gates read rows.
  • The concurrent mode no longer emits CREATE INDEX CONCURRENTLY or DROP INDEX CONCURRENTLY for an index of a partitioned table. PostgreSQL rejects
    both, so such a plan could not be verified. A new non-unique index is built
    with an ON ONLY parent, a concurrent build for each leaf partition, and an
    attach. A unique index and every drop use the plain statement, with the
    hazard partitioned_index_not_concurrent.

Added

  • A login role that only inherits predefined read-only roles is a valid
    observer for drift check and contract check: pg_read_all_data,
    pg_monitor, pg_read_all_settings, pg_read_all_stats, and
    pg_stat_scan_tables. This is the role that the role tool of a managed
    provider can create, for example pscale role create DATABASE BRANCH NAME --inherited-roles pg_read_all_data. Before, such a role stopped with
    observer_role_elevated, and only the database owner or a role with direct
    grants on every relation passed. The result reports
    observer.mode: predefined_read_role. The role must still have none of
    SUPERUSER, CREATEDB, CREATEROLE, and REPLICATION. A membership in any
    other predefined role (for example pg_write_all_data), in a role that can
    log in, or with ADMIN OPTION is still refused. See the
    observer role.
  • An observer login role can have BYPASSRLS, reported as
    observer.bypass_rls: true. Row-level security with no policy for a reader
    hides every row from a pg_read_all_data role with no error, so a team
    needs BYPASSRLS on its read-only role. Before, BYPASSRLS stopped the
    command with observer_role_elevated.
  • The error of drift check and the findings of contract check for a
    refused or incomplete observer carry next_actions with the SQL for the two
    valid forms of role.
  • verify explains the failure of a non-transactional batch with SQLSTATE
    25001: PostgreSQL runs SQL that holds more than one statement in one
    implicit transaction, so each statement that cannot run in a transaction
    block needs its own -- onwardpg:batch nontransactional line in an edited
    phase.

Changed

  • The observer guard now proves that a role that is not the database owner is
    read-only. It reads the effective privileges of the role and of each role
    that it is a member of, and stops with observer_access_policy_unsafe when
    one of them can write to a relation or a sequence, has CREATE on the
    database or on a schema, or owns an object. A grant to PUBLIC counts.
    Before, the guard read only the schema grants and the table grants of the
    observer's own roles, so a write privilege through PUBLIC (for example
    CREATE on public in a database made before PostgreSQL 15) or on a
    sequence passed. An observer that was accepted with such a privilege is now
    refused; revoke the privilege or use the database owner.
  • A history replay for drift check, plan, or draft now stops when the
    scratch server is not the PostgreSQL major of the history receipts, as
    verify does. It also runs the manual verification queries and the
    verify.sql assertions of each bundle, which already passed when the bundle
    was verified.
  • The timing stage for these replays is history_replay; ddl_execute now
    measures only the load of exported DDL.
  • A bundle keeps its index lock mode. The mode was stored in the manifest
    (planner.options.concurrent_indexes) but never read: a bundle planned with
    --concurrent-indexes went back to plain CREATE INDEX and DROP INDEX in
    transactional batches on the next plain onwardpg plan, with no message.
    plan and draft now use the flag of the run, then the stored choice of
    the bundle, then the target configuration, then the blocking default.
    --concurrent-indexes=false selects the blocking mode.
  • New optional target key concurrent_indexes = true in .onwardpg.toml.
    plan, draft, and dev plan then use the concurrent mode without the
    flag. init does not read the key. config check reports the key.
  • plan and draft report index_lock_mode (concurrent_indexes, source,
    and, after a change, previous_concurrent_indexes). Two new warning codes:
    index_lock_mode_changed when a run writes a bundle in another mode than
    its previous generation, and index_lock_mode_differs_from_config when a
    bundle stays blocking although the target is configured as concurrent.
  • The removal of an index is no longer data_loss. A standalone index that
    enforces nothing is dropped without a decision hint, in contract, with
    safety=review and the hazards slower_queries_possible and
    blocking_lock or concurrent_index_drop. Before, each one was
    safety=dangerous, data_loss, and needed its own drop hint.
  • Every drop of a unique index or of a constraint keeps one decision. Its
    choice hazard is now the enforcement that is removed (for a unique index:
    unique_index_enforcement_removed, duplicate_rows_possible), not
    data_loss. An index that is the replica identity of its table, or that the
    table is clustered on, also keeps its decision.
  • A drop hint for an index that needs no decision is accepted
    and reported as the finding hint_not_needed.
  • next_actions has a semantic_hints_file action when two or more decisions
    have exactly one choice. It lists the complete hints array for
    --hints-file.
  • The help text of --concurrent-indexes states the whole effect: create and
    drop, with CONCURRENTLY, in nontransactional batches.

Compatibility

  • No bundle file has a new member. Bundles in accepted history verify with no
    change to a byte: verify replays the stored SQL and does not plan it
    again.
  • A new plan can have more batches than the same plan from preview.7: one for
    each statement that cannot run in a transaction block. The statements and
    their order are the same. The format of plan.json and of the phase files
    is unchanged, and a bundle from preview.7 with several such statements in
    one batch still verifies with no change to a byte. If such a preview.7
    bundle was edited and is planned again, the phase reconciliation can report
    a conflict, because the generated text around the edit changed; resolve it
    in the phase file.
  • A bundle that is planned again gets the new classification, so its
    plan.json, phase SQL comments, and digests change. This is intended for a
    bundle that is not yet in accepted history. Stored answers for index drops
    that need no decision now are dropped from the bundle on that replan.

v0.1.0-preview.7

v0.1.0-preview.7 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 07 Oct 14:06
0772dac

Changed

  • plan, draft, verify, and init are about five times faster on a large
    schema. On a schema of 271 tables, about 3,500 constraints and about 1,170
    indexes, with a one-column change, plan went from 126–149 s to 23–26 s,
    verify from 114 s to 22 s, and verify --check from 107 s to 20 s on the
    same machine. Plans, phase SQL, and catalog fingerprints are byte-identical.
    The changes:
    • The catalog reader no longer searches every snapshot object once for each
      column, and a snapshot keeps its sorted object order between writes. These
      two loops were quadratic and cost about 7 seconds for each catalog read of
      that schema.
    • These commands run the export two times, not four: once at the start, and
      once immediately before they write or report. Byte-identical output proves
      that the export is deterministic and that it did not change while the
      command worked, and the command does not load identical DDL into scratch
      PostgreSQL a second time. Output that differs is handled as before: a
      third run decides whether the export is nondeterministic, and a changed
      export must have the same catalog fingerprint. A nondeterministic export
      is therefore reported at the end of the command, not at the start.
    • A command that ends without a write (for example unsupported,
      no_changes, or a failed verification) now stops with an error if the
      export changed while it worked. Before, it reported a result for the
      export it read at the start.
    • One command no longer replays the same SQL more often than its checks
      need. A verification still compares two separate executions in two
      databases; they now run at the same time. A second verification in the
      same command (expand checkpoint, then complete bundle) reuses an execution
      of the first only when both run exactly the same SQL with the same checks.
      plan used seven scratch databases and five history replays; it now uses
      four and three.
    • Base history replay runs during the first export.
  • dev, config check, and diff --target still run the export twice back to
    back.
  • onwardpg no longer checks what schema_command writes to the checkout. The
    earlier check read the bytes of every file outside .git and node_modules
    before and after each export run, four times for each command, and stopped
    with DDL export command modified repository inputs when any file differed.
    It is removed, with that error. A result depends on the DDL that the export
    prints: the two export runs of a command must give the same bytes, and the
    result is proved by replay on PostgreSQL. The removed check added one thing
    to that, a stop when the command also wrote files, which does not make a
    plan wrong. It cost 8 to 10 seconds for each command in a clean checkout of
    50,000 files, and 96 seconds for each of the four walks in a checkout with
    1.3 million paths and 24 GB of caches and build outputs. It also stopped the
    command whenever another process (a build, a development server, another
    agent) wrote a file during an export run. On the schema above, in
    interleaved runs on one machine, plan went from 24–27 s to 16–19 s (27 s
    in one run, whose two export runs took 17 s), verify from 20–24 s to
    15 s, and verify --check from 27–31 s to 16–22 s. Other work can now run
    in the same checkout while a command runs. The fingerprint was never
    stored, so bundle formats and digests are unchanged. The
    safety model
    states what carries the guarantee and what is no longer caught.

Added

  • A result document can have a warnings member, which never changes the
    status or the exit code. The first warning code is export_side_effects:
    in a git work tree, a command that runs schema_command compares
    git status from before the first export run with git status after the
    last, and lists the paths whose status changed. Files that git ignores are
    not listed, and without git there is no observation. See
    warnings.
  • ONWARDPG_TIMINGS=1 writes one JSON line to standard error with the wall
    time and run count of each stage of the command (export runs, scratch
    databases, history replays, catalog reads, planner).
    See performance.

v0.1.0-preview.6

v0.1.0-preview.6 Pre-release
Pre-release

Choose a tag to compare

@github-actions github-actions released this 06 Oct 18:51
f13e399

Fixed

  • drift check no longer hides live catalog state that makes init, plan, and
    diff stop as unsupported. The state (for example
    ownership:extension:earthdistance=pscale_admin or
    parameter_acl:session_replication_role) was in the inspected snapshot, but
    the report dropped it and showed only drifted. The report now has an
    unsupported list; when it is not empty the status is unsupported and the
    exit code is 3, and the differences are still listed. A result without
    unsupported entries now means there is no modeled drift and nothing the
    planner would refuse. The list includes unsupported state in the replayed
    history as well as in the live catalog.

  • contract check reported unsupported production state as catalog_drift
    (status blocked, exit 4), because that state is part of the observed
    fingerprint. It now reports status unsupported (exit 3), the selectors in
    unsupported, and the finding unsupported_catalog_state, and runs no data
    gate. Catalog drift is classified on the modeled graph alone, so a catalog
    that also differs from the receipted checkpoint still gets its
    catalog_drift, expand_not_applied, or contract_already_applied finding.

  • A foreign key whose only differences from the desired schema are its own
    name and the name of the index behind the referenced primary-key or unique
    constraint is now offered as a rename_constraint decision, in the same round as the key's own
    rename. Before, such a foreign key was never a rename candidate, because the
    index name that PostgreSQL records for the referenced key (UsingIndex) was
    compared literally: after an ORM or pg_dump round trip left old key names
    (..._pk, idx_..._sqlite_autoindex_...), every foreign key referencing one
    was planned as drop plus re-add, with a full re-validation on a production
    table. The foreign key is a candidate only while its referenced key is itself
    a rename candidate that has not been declined; every other catalog field is
    still compared. A confirmed pair emits only ALTER TABLE ... RENAME CONSTRAINT, which PostgreSQL carries out as a metadata change. A foreign
    key that references a unique index without a constraint is not covered.

Added

  • A target can list live_ignore selectors in .onwardpg.toml for state a
    managed PostgreSQL provider owns in live clusters: an extension or a schema
    owned by another role (ownership:extension:NAME=ROLE,
    ownership:schema:NAME=ROLE) and a pg_parameter_acl grant
    (parameter_acl:NAME). Those selectors can exist only in production, so they
    could not be target ignore entries and every live command needed one
    --ignore flag per selector. drift check, contract check, and diff
    (with the new --target NAME [--config FILE]) remove exactly the named
    blocker markers from the live snapshot and list them as
    observer.live_ignored (workspace_compatibility for a diff plan). Selectors are
    exact, written as the report prints them with quote_ident-style
    identifiers, validated for form and kind, and need not match in every
    cluster. They never remove a typed object or add an ignore receipt.
    They are removed before the comparison fingerprint is computed, so a catalog
    that differs from the expected graph only by acknowledged state compares
    equal (drift check is drift_free; contract check matches its
    checkpoint) and the reported fingerprint depends on the list;
    observer.observed_fingerprint reports the fingerprint before removal, in
    drift check and contract check results when something was removed.
    A configured selector that matched nothing is listed as
    observer.live_ignore_unmatched (live_ignore_unmatched:SELECTOR in a diff
    plan's workspace_compatibility); that is information only and never changes
    a status, exit code, or fingerprint, so a typo, or a keyword that PostgreSQL
    quotes, does not pass unnoticed. --target and --config belong to diff
    only: the legacy plan --from --to spelling does not accept them and never
    applies the list.
    Planning, verification, replayed history, and DDL sources ignore the list. The attribute ledger keeps these catalogs classified as blocked:
    a parameter ACL or a foreign-owned schema can be a real privilege difference
    between environments, so nothing is acknowledged without an exact entry, and a
    changed owner or a new grant blocks again.
  • diff --from-env ENV and --to-env ENV read a side's PostgreSQL URL from an
    environment variable, like drift check --database-env, so a live URL stays
    out of process arguments. Each is mutually exclusive with --from or --to
    for the same side.

Changed

  • The dependency audit of the documentation website runs in a scheduled
    workflow, not in pull request CI or in the release workflow. The website is
    not part of the binary.