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
9 changes: 8 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ PG_DATABASE ?= pgsprite
# Localhost-only test credentials, parameterized above — not a real secret.
PG_DSN_LOCAL = postgres://$(PG_USER):$(PG_PASSWORD)@localhost:$(PG_PORT)/$(PG_DATABASE)?sslmode=disable# sadscan:disable np.postgres.1

.PHONY: build test test-unit test-db test-supported-postgres test-aws-boundary lint setup db-up db-down clean demo demo-seed demo-check
.PHONY: build test test-unit test-db test-supported-postgres test-aws-boundary lint setup db-up db-down demos clean demo demo-seed demo-check

build:
$(GO) build -o bin/pg-sprite ./cmd/pg-sprite
Expand Down Expand Up @@ -59,6 +59,13 @@ db-up:
db-down:
$(COMPOSE_ENV) $(COMPOSE) -f compose/compose.yml down -v

# Re-render the README/docs demo GIFs from their VHS tapes (docs/demos).
# Needs vhs (brew install vhs); the binary and compose database are built
# and started as prerequisites — every tape puts bin/ on PATH, so rendering
# without a built binary would record "command not found" into the GIFs.
demos: build db-up
cd docs/demos && for t in *.tape; do vhs $$t || exit 1; done

clean:
rm -rf bin

Expand Down
79 changes: 43 additions & 36 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,49 @@ Every 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.

![pg-sprite replacing a blocking ADD CONSTRAINT with the safer online sequence: dry-run, real run, then the catalog proof](docs/demos/improve.gif)

Animated demos for the other routes — declarative diff, refusal with typed
help, offline lint — live in [docs/demos/](docs/demos/), rendered from committed
[VHS](https://github.com/charmbracelet/vhs) tapes (`make demos` re-renders
them).

**Diff: declarative desired state in, classified plan out.** Point at a
reviewed `CREATE TABLE` file and get the statements that converge the live
table onto it, reported in the same diagnostic grammar as the dry run.
`--sql` prints the plan as an executable SQL script instead, and a plan
containing a statement execution would refuse exits 2 — the same CI gate
as the dry run:

```console
$ pg-sprite diff --desired users.sql
statement 1:
ALTER TABLE public.users ADD COLUMN nickname text;

note[metadata-only]:
ADD COLUMN nickname — a brief catalog-only change; takes a short
exclusive lock but does not scan or rewrite the table

note:
runs as written

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

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

diff:
nothing was executed

sql:
re-run with --sql to print the plan as an executable SQL script

apply:
run each statement via pg-sprite migrate --alter '…', which refuses
blocking forms and substitutes safer online sequences
```

**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):
Expand Down Expand Up @@ -144,42 +187,6 @@ lint:
changes.sql — 1 finding, 0 errors, 1 warning
```

**Diff: declarative desired state in, classified plan out.** Point at a
reviewed `CREATE TABLE` file and get the statements that converge the live
table onto it, reported in the same diagnostic grammar as the dry run.
`--sql` prints the plan as an executable SQL script instead, and a plan
containing a statement execution would refuse exits 2 — the same CI gate
as the dry run:

```console
$ pg-sprite diff --desired users.sql
statement 1:
ALTER TABLE public.users ADD COLUMN nickname text;

note[metadata-only]:
ADD COLUMN nickname — a brief catalog-only change; takes a short
exclusive lock but does not scan or rewrite the table

note:
runs as written

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

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

diff:
nothing was executed

sql:
re-run with --sql to print the plan as an executable SQL script

apply:
run each statement via pg-sprite migrate --alter '…', which refuses
blocking forms and substitutes safer online sequences
```

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
49 changes: 49 additions & 0 deletions docs/demos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Terminal demos

VHS tapes for the README/docs demo GIFs. Each tape is deterministic and
rerunnable: hidden setup reseeds the database state it needs, so a re-render
after a CLI output change is just `make demos` (or `vhs <tape>` from this
directory).

## Prerequisites

- [VHS](https://github.com/charmbracelet/vhs) (`brew install vhs`)
- A built binary: `make build`
- The compose database for the database-backed tapes: `make db-up`
(`lint.tape` is fully offline)

## 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 |

## Re-rendering

```sh
make build db-up
make demos
```

The GIFs are committed next to the tapes so the README renders without any
build step. Nothing pins them: renderer tests pin
`docs/cli-output-examples.md` and the contract docs, and CI's demo smoke
test asserts on `demo/tour.sh` output, but the README samples and these
GIFs drift silently when CLI output changes — re-rendering is a manual
duty, done in the same change that alters the output. Each re-render
commits whole new binary blobs (no deltas), so re-render only the tapes
whose recorded output actually changed; if the accumulated weight starts
to bite, the GIFs move to release assets.

## Tape-writing notes

- The tape parser has no escapes inside `"..."` — use backtick strings for
commands containing double quotes; `\n` types literally (use one `echo` per
line instead of `printf '\n'`; a single `\d` is fine for psql).
- Keep a `Sleep` between the hidden `clear` and `Show`, or capture resumes
before the screen clears and setup commands leak into the first frames.
- The prompt is set in hidden setup so recorded frames show only the command
and its output.
Binary file added 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.
37 changes: 37 additions & 0 deletions docs/demos/diff-greenfield.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Diff, brand-new table: absent from the live database, plans as the full
# desired schema, reported in the diagnostic grammar with the greenfield note.
# Render from this directory: vhs diff-greenfield.tape (or `make demos` at the repo root).
# Needs: make build && make db-up first. Hidden setup drops widgets so the
# tape is deterministic and rerunnable.
Output diff-greenfield.gif

Set Shell "bash"
Set FontSize 14
Set Width 1300
Set Height 800
Set Padding 16
Set TypingSpeed 30ms

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 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 "clear" Enter
Sleep 1.5s
Show

Type "cat widgets.sql"
Sleep 500ms
Enter
Sleep 2s
Enter
Enter
Sleep 500ms

Type "pg-sprite diff --desired widgets.sql"
Sleep 500ms
Enter
Sleep 8s
Binary file added 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.
89 changes: 89 additions & 0 deletions docs/demos/improve.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Improve (the README hero): dry-run, real run, proof in the catalog — then
# the same change declaratively: diff plans from a desired-state file,
# migrate executes, diff confirms convergence.
# Render from this directory: vhs improve.tape (or `make demos` at the repo root).
# Needs: make build && make db-up first. Hidden setup reseeds the users table
# and writes the desired-state file, so the tape is deterministic and
# rerunnable.
Output improve.gif

Set Shell "bash"
Set FontSize 14
Set Width 1300
Set Height 1160
Set Padding 16
Set TypingSpeed 30ms

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 `demo_dir="$(mktemp -d)" && cd "$demo_dir"` Enter
Type "echo 'CREATE TABLE users (' > users.sql" Enter
Type "echo ' id bigint PRIMARY KEY,' >> users.sql" Enter
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 "clear" Enter
Sleep 1.5s
Show

Type "pg-sprite migrate --alter 'ALTER TABLE users ADD CONSTRAINT users_email_key UNIQUE (email)' --dry-run"
Sleep 500ms
Enter
Sleep 6s
Enter
Enter
Sleep 500ms

Type "pg-sprite migrate --alter 'ALTER TABLE users ADD CONSTRAINT users_email_key UNIQUE (email)'"
Sleep 500ms
Enter
Sleep 10s
Enter
Enter
Sleep 500ms

Type "docker exec compose-postgres-1 psql -U pgsprite -d pgsprite -c '\d users'"
Sleep 500ms
Enter
Sleep 8s
Enter
Enter
Sleep 500ms

Type "clear"
Sleep 500ms
Enter
Sleep 1s

Type "cat users.sql"
Sleep 500ms
Enter
Sleep 2s
Enter
Enter
Sleep 500ms

Type "pg-sprite diff --desired users.sql"
Sleep 500ms
Enter
Sleep 6s
Enter
Enter
Sleep 500ms

Type "pg-sprite migrate --alter 'ALTER TABLE users ADD COLUMN nickname text'"
Sleep 500ms
Enter
Sleep 6s
Enter
Enter
Sleep 500ms

Type "pg-sprite diff --desired users.sql"
Sleep 500ms
Enter
Sleep 8s
Binary file added 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.
50 changes: 50 additions & 0 deletions docs/demos/lint.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Lint: offline, no database. unset PGSPRITE_URL on camera to make the point,
# then lint a two-statement change file.
# Render from this directory: vhs lint.tape (or `make demos` at the repo root).
# Needs: make build only — no database.
Output lint.gif

Set Shell "bash"
Set FontSize 14
Set Width 1560
Set Height 1160
Set Padding 16
Set TypingSpeed 30ms

Hide
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 "clear" Enter
Sleep 1.5s
Show

Type "unset PGSPRITE_URL"
Sleep 500ms
Enter
Sleep 1s
Enter
Enter
Sleep 500ms

Type "cat changes.sql"
Sleep 500ms
Enter
Sleep 2s
Enter
Enter
Sleep 500ms

Type "pg-sprite lint changes.sql"
Sleep 500ms
Enter
Sleep 8s

# Cleanup off camera. mktemp -d would not leave a file behind, but lint
# prints the absolutized file path in its finding labels, so recording from
# a mktemp dir would put /var/folders noise into the GIF; /tmp keeps the
# label short and this removes the file.
Hide
Type "rm -f /tmp/changes.sql" Enter
Binary file added 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.
35 changes: 35 additions & 0 deletions docs/demos/refuse.tape
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Refuse with typed help and the CI exit-code contract:
# error[rewrite-required] + note[safer-idiom] + help[name-constraint-then-validate],
# doc anchors, and echo $? showing 2.
# Render from this directory: vhs refuse.tape (or `make demos` at the repo root).
# Needs: make build && make db-up first. Hidden setup reseeds the users table.
Output refuse.gif

Set Shell "bash"
Set FontSize 14
Set Width 1300
Set Height 760
Set Padding 16
Set TypingSpeed 30ms

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 "clear" Enter
Sleep 1.5s
Show

Type "pg-sprite migrate --alter 'ALTER TABLE users ADD CHECK (id > 0)' --dry-run"
Sleep 500ms
Enter
Sleep 8s
Enter
Enter
Sleep 500ms

Type "echo $?"
Sleep 500ms
Enter
Sleep 5s
Loading