Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 12 additions & 86 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ rules that apply inside the core. Read it before changing anything under

## What it looks like

Every sample below is captured verbatim from a real session against the
The sample below is captured verbatim from a real session against the
compose database (`make db-up`, PostgreSQL 16): `$` marks the command,
everything after it is the tool's output.

Expand Down Expand Up @@ -93,99 +93,25 @@ apply:

**Improve: a blocking form is replaced with the safer online sequence.**
`migrate --dry-run` shows exactly what would run, as compiler-style
diagnostics with a doc anchor per finding (exit 0 — the plan is executable):

```console
$ pg-sprite migrate --alter 'ALTER TABLE users ADD CONSTRAINT users_email_key UNIQUE (email)' --dry-run
statement 1:
ALTER TABLE users ADD CONSTRAINT users_email_key UNIQUE (email);

warning[safer-idiom]:
ADD CONSTRAINT users_email_key — holds a blocking lock on the table for
the whole operation — writes (and for some forms reads) wait until it
finishes

note:
pg-sprite will run a safer online sequence instead:
1. CREATE UNIQUE INDEX CONCURRENTLY "users_email_key" ON "users" ("email");
2. ALTER TABLE "users" ADD CONSTRAINT "users_email_key" UNIQUE USING INDEX "users_email_key";

note:
each step commits on its own — not transactionally equivalent, and the
sequence must not run inside a transaction block

docs:
https://github.com/block/pg-sprite/blob/main/docs/postgres-online-ddl-reference.md#safer-idiom

plan:
public.users (PostgreSQL 16.14) — 1 statement, 2 steps to run, 0 refused

dry-run:
nothing was executed

apply:
re-run without --dry-run
```
diagnostics with a doc anchor per finding (exit 0 — the plan is executable).
The demo above records the whole flow — dry run, real run, catalog proof;
the machine-readable shape is in
[docs/cli-output-examples.md](docs/cli-output-examples.md).

**Refuse: no safe path exists, so nothing runs.** A genuine table rewrite
needs the copy-and-swap backend (a later phase); the dry run exits 2 so CI
can gate on it without parsing JSON. The exit-code gate stops refusals only —
a destructive-but-executable change (`DROP COLUMN`) warns and exits 0, so a
gate that must stop drops checks `.statements[].destructive` in the
`--json` report:

```console
$ pg-sprite migrate --alter 'ALTER TABLE users ALTER COLUMN id TYPE text' --dry-run
statement 1:
ALTER TABLE users ALTER COLUMN id TYPE text;

error[backend-unavailable]:
refused — needs the copy-and-swap backend (an online shadow-table copy
with a cutover), which this build does not implement yet

note[type-rewrite]:
ALTER COLUMN id TYPE text — the type conversion forces a full table
rewrite under an exclusive lock that blocks reads and writes

docs:
https://github.com/block/pg-sprite/blob/main/docs/postgres-online-ddl-reference.md#backend-unavailable
https://github.com/block/pg-sprite/blob/main/docs/postgres-online-ddl-reference.md#type-rewrite

plan:
public.users (PostgreSQL 16.14) — 1 statement, 0 steps to run, 1 refused

dry-run:
nothing was executed
```
`--json` report. Watch it in
[docs/demos/refuse.gif](docs/demos/refuse.gif); the machine-readable shape
is in [docs/cli-output-examples.md](docs/cli-output-examples.md).

**Lint: offline, no database needed.** Flag blocking idioms in a DDL file
and suggest the safer form:

```console
$ pg-sprite lint changes.sql
changes.sql:1:1:
CREATE INDEX users_email_idx ON users (email);

warning[blocking-idiom]:
CREATE INDEX users_email_idx — holds a blocking lock on the table for
the whole operation — writes (and for some forms reads) wait until it
finishes

help:
a safer online form exists — not a semantic equivalent, and running it
by hand forgoes the engine's execution-time guards:
1. CREATE INDEX CONCURRENTLY users_email_idx ON users USING btree (email);

note:
run each statement in its own transaction, never one block; after a
failed CONCURRENTLY build, check pg_index.indisvalid and rebuild

docs:
https://github.com/block/pg-sprite/blob/main/docs/postgres-online-ddl-reference.md#safer-idiom

lint:
changes.sql — 1 finding, 0 errors, 1 warning
```
and suggest the safer form — no connection, no Docker; error-severity
findings exit non-zero, warnings alone pass. Watch it in
[docs/demos/lint.gif](docs/demos/lint.gif); the machine-readable shape is
in [docs/cli-output-examples.md](docs/cli-output-examples.md).

More shapes — every disposition as JSON, destructive warnings, and exit
codes — are in [docs/cli-output-examples.md](docs/cli-output-examples.md).
Expand Down
5 changes: 3 additions & 2 deletions docs/cli-output-examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
Representative, real JSON outputs for every shape the CLI produces: the
plan report for each dry-run disposition, the execution verdict, the
linter, and diff. The human text rendering of the same reports is display
only — see the [README](../README.md) for samples; the JSON is the machine
contract. All were captured verbatim from a real session against the
only — see the [README](../README.md)'s diff sample and the animated demos
in [demos/](demos/) for how it reads; the JSON is the machine contract.
All were captured verbatim from a real session against the
compose database (`make db-up`, PostgreSQL 16) — `$` marks the command,
everything after it is the tool's output — with this schema:

Expand Down
35 changes: 29 additions & 6 deletions docs/demos/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,35 @@ directory).

## Tapes

| Tape | Story | Needs database |
|---|---|---|
| `diff-greenfield.tape` | Declarative diff for a table absent from the live database: the full desired schema planned as a diagnostic report with the greenfield note | yes |
| `improve.tape` | Dry-run of a blocking `ADD CONSTRAINT … UNIQUE`, the real run executing the safer online sequence, `\d users` catalog proof — then the declarative loop: `diff --desired` plans the remaining change, `migrate` executes it, `diff` confirms convergence | yes |
| `refuse.tape` | `error[rewrite-required]` refusal with typed `note`/`help` diagnostics and doc anchors, then `echo $?` showing the exit-code contract (2) | yes |
| `lint.tape` | Offline lint of a two-statement change file — `unset PGSPRITE_URL` on camera to show no database is needed | no |
### `diff-greenfield.tape` — declarative diff, greenfield (needs database)

Declarative diff for a table absent from the live database: the full
desired schema planned as a diagnostic report with the greenfield note.

![pg-sprite diff planning a CREATE TABLE from the full desired schema, with the greenfield note](diff-greenfield.gif)

### `improve.tape` — the safer online sequence, then the declarative loop (needs database)

Dry-run of a blocking `ADD CONSTRAINT … UNIQUE`, the real run executing
the safer online sequence, `\d users` catalog proof — then the declarative
loop: `diff --desired` plans the remaining change, `migrate` executes it,
`diff` confirms convergence.

![pg-sprite replacing a blocking ADD CONSTRAINT with the safer online sequence, then converging on a desired schema via diff and migrate](improve.gif)

### `refuse.tape` — refusal and the exit-code contract (needs database)

`error[rewrite-required]` refusal with typed `note`/`help` diagnostics and
doc anchors, then `echo $?` showing the exit-code contract (2).

![pg-sprite refusing a change with no safe path, with typed diagnostics and exit code 2](refuse.gif)

### `lint.tape` — offline lint (no database)

Offline lint of a two-statement change file — `unset PGSPRITE_URL` on
camera to show no database is needed.

![pg-sprite lint flagging blocking idioms in a DDL file with no database connection](lint.gif)

## Re-rendering

Expand Down
Binary file modified docs/demos/diff-greenfield.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/demos/diff-greenfield.tape
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Type "export PGSPRITE_URL='postgres://pgsprite:pgsprite@localhost:5432/pgsprite?
Type "docker exec compose-postgres-1 psql -U pgsprite -d pgsprite -c 'DROP TABLE IF EXISTS widgets' >/dev/null" Enter
Type `demo_dir="$(mktemp -d)" && cd "$demo_dir"` Enter
Type "echo 'CREATE TABLE widgets (id bigint PRIMARY KEY, name text NOT NULL, price numeric);' > widgets.sql" Enter
Type "export PS1='\n~/pg-sprite main '" Enter
Type "export PS1='\n~/kiran01bm/github/pg-sprite main '" Enter
Type "clear" Enter
Sleep 1.5s
Show
Expand Down
Binary file modified docs/demos/improve.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/demos/improve.tape
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Type "echo ' email text,' >> users.sql" Enter
Type "echo ' nickname text,' >> users.sql" Enter
Type "echo ' CONSTRAINT users_email_key UNIQUE (email)' >> users.sql" Enter
Type "echo ');' >> users.sql" Enter
Type "export PS1='\n~/pg-sprite main '" Enter
Type "export PS1='\n~/kiran01bm/github/pg-sprite main '" Enter
Type "clear" Enter
Sleep 1.5s
Show
Expand Down
Binary file modified docs/demos/lint.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/demos/lint.tape
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Type `export PATH="$(git rev-parse --show-toplevel)/bin:$PATH"` Enter
Type "cd /tmp" Enter
Type "echo 'CREATE INDEX users_email_idx ON users (email);' > changes.sql" Enter
Type "echo 'ALTER TABLE users ADD CONSTRAINT users_email_key UNIQUE (email);' >> changes.sql" Enter
Type "export PS1='\n~/pg-sprite main '" Enter
Type "export PS1='\n~/kiran01bm/github/pg-sprite main '" Enter
Type "clear" Enter
Sleep 1.5s
Show
Expand Down
Binary file modified docs/demos/refuse.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion docs/demos/refuse.tape
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Hide
Type `export PATH="$(git rev-parse --show-toplevel)/bin:$PATH"` Enter
Type "export PGSPRITE_URL='postgres://pgsprite:pgsprite@localhost:5432/pgsprite?sslmode=disable'" Enter
Type `docker exec compose-postgres-1 psql -U pgsprite -d pgsprite -c 'DROP TABLE IF EXISTS users' -c 'CREATE TABLE users (id bigint PRIMARY KEY, email text)' -c "INSERT INTO users SELECT g, 'u'||g||'@x.com' FROM generate_series(1,1000) g" >/dev/null` Enter
Type "export PS1='\n~/pg-sprite main '" Enter
Type "export PS1='\n~/kiran01bm/github/pg-sprite main '" Enter
Type "clear" Enter
Sleep 1.5s
Show
Expand Down
Loading