pgdeclare
pgdeclare is a Postgres schema differ.
Why we built it: Your Database Schema Should Be Code You Can Read.
Overview
Your database schema should be code you can read.
With migration files, reviewers read deltas, not the schema, and an LLM reading the repository has to reconstruct the state too, getting it wrong in the same places people do. pgdeclare keeps the schema as declarations: one CREATE TABLE per table, in plain SQL files, always describing the database as it is now. You change a table by editing its definition. pgdeclare builds the declared schema in a scratch database, diffs it against the live one through pg_catalog, and applies the difference.
A diff cannot infer everything, such as a rename or a backfill. For those you write small imperative @migrate-before / @migrate-after blocks next to the DDL they belong to. Each runs once per database and is recorded in a hash ledger, so an edit after it ran is refused rather than silently ignored. Destructive changes on tables that hold data need an explicit @drop directive. Migrations that have run can be pruned so they don't pile up. History still exists, in git, where history belongs.
Status: 0.1.1. The engine has run the production schema of Powertime since it went into service on 10 December 2025. Backwards compatibility may not be preserved until 1.0.
Design choices
- Postgres parses your SQL. The desired schema is not reconstructed from a parser's model of your files. pgdeclare executes them in a scratch "plan" database and introspects the result, so defaults, implicit constraints and type normalisation come out exactly as Postgres would produce them.
- Postgres orders the work. Statements, and the changes themselves, run retry-until-stable: anything that fails on a missing dependency is retried after the rest, until everything succeeds or no progress is made. You do not maintain a dependency order.
Install
pip install pgdeclare
# or, inside a uv project
uv add pgdeclare
Requires Python 3.12+ and a PostgreSQL 14 to 18 server.
Connect as a role that owns the target database's schemas. Each run builds your schema in a scratch plan database on the same server, which by default is created and dropped per run, so the role needs CREATEDB. Without it, create a plan database you own (createdb -O app app_plan) and pass --plan-database app_plan.
Quickstart
A schema tree:
sql/
└── schemas/
├── 01_types.sql
└── 02_note.sql
-- sql/schemas/01_types.sql
CREATE TYPE mood AS ENUM ('calm', 'loud');
-- sql/schemas/02_note.sql
CREATE TABLE IF NOT EXISTS note (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
author text NOT NULL,
body text NOT NULL,
mood mood NOT NULL DEFAULT 'calm'
);
CREATE INDEX IF NOT EXISTS note_author_idx ON note (author);
Command line
export DATABASE_URL=postgresql://app:secret@localhost:5432/app
pgdeclare diff # what would change, as a summary plus SQL
pgdeclare diff --json # the same, machine-readable
pgdeclare diff --exit-code # exit 2 when anything is pending (for CI)
pgdeclare apply --dry-run # plan only, nothing written
pgdeclare apply --dry-run --prove # rehearse the real run on a throwaway clone
pgdeclare apply # converge the database
Now add a column and a backfill:
-- sql/schemas/02_note.sql
CREATE TABLE IF NOT EXISTS note (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
author text NOT NULL,
body text NOT NULL,
mood mood NOT NULL DEFAULT 'calm',
title text
);
CREATE INDEX IF NOT EXISTS note_author_idx ON note (author);
-- @migrate-after:2026-01-15_note_titles
UPDATE note SET title = left(body, 40) WHERE title IS NULL;
-- @end-migrate
pgdeclare apply adds the column, then runs the block once and records it.
Every command, flag and exit code is in docs/cli.md; the library API is in docs/python.md.
Documentation
- Command line: flags, exit codes,
--prove. - Python API: the library entry points and their errors.
- Schema files: directory layout,
@migrate-before/@migrate-after,@drop,DOblocks. - Coverage: every object kind pgdeclare manages, how, and what it leaves alone.
- Configuration: every
PgDeclareConfigfield. - Safety: previews,
--prove, online/offline/blocked changes, quoting, masking, and the supported PostgreSQL versions (14 to 18).
How it compares
As of October 2026, pgdeclare manages nearly every object kind pg-delta and pgschema do; the exact differences are in Coverage. pgdeclare's difference is where the steps a diff cannot infer live: next to the DDL they belong to, run once, and recorded by hash.
| Tool | Renames and backfills | Records what ran | Free licence | Notes |
|---|---|---|---|---|
| pgdeclare | @migrate blocks beside the DDL |
Hash ledger; edited blocks are refused | Apache-2.0 | Re-diffs after applying and fails the run if anything is left; --prove rehearses the full run, @migrate blocks included, on a clone |
| pgschema | Outside the tool | No; state only | Apache-2.0 | Terraform-style plan and apply; broad object coverage |
| pg-delta | Renames come out as drop plus add, edited by hand; never writes row changes | Through Supabase migration files | PostgreSQL | Alpha; very broad coverage; prove tests a plan on a clone first |
| Atlas | Versioned migration files | Yes, in versioned mode | Apache-2.0 Community Edition | The Community Edition omits views, functions, triggers and RLS policies; the full binary is under the Atlas MSA |
| pg-schema-diff | Renames come out as drop plus add | No | MIT | Strong lock avoidance for online changes |
| migra | Not handled | No | Unlicense | Deprecated; last release 2022 |
Contributing
Issues and pull requests are welcome on GitHub. See CONTRIBUTING.md for running the tests, and SECURITY.md for reporting vulnerabilities.
License
Apache License 2.0. See LICENSE and NOTICE.
Extracted from Powertime, where it manages the application's schema.
Metadata
Release files for pgdeclare 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pgdeclare-0.1.1.tar.gz | 252.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pgdeclare-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 399.8 kB
Release files / pgdeclare-0.1.1.tar.gz
| Download URL | pgdeclare-0.1.1.tar.gz |
|---|---|
| Size | 252.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
44879e2c5a08978f00340b33253cd77bfd8158af523ebfbbd854f6a6e0aecb64
|
|
BLAKE2b-256 checksum How to use checksums |
1dbc78331016730d0404e8a4603d082a38cada8d0c8012e157168ffc837fd689
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency logRelease files / pgdeclare-0.1.1-py3-none-any.whl
| Download URL | pgdeclare-0.1.1-py3-none-any.whl |
|---|---|
| Size | 147.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c019f79718e17264c84c063683ea21e3046eead234fb061f4da74941b2a56163
|
|
BLAKE2b-256 checksum How to use checksums |
89b1bffaa7d1b5eb99f116209a3b1c8f0f68d51000993976daa7f854a3f586a5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 18, 2026.
Transparency log